diff options
author | Jonas Franz <info@jonasfranz.software> | 2019-03-08 17:42:50 +0100 |
---|---|---|
committer | techknowlogick <matti@mdranta.net> | 2019-03-08 11:42:50 -0500 |
commit | e777c6bdc6f12f9152335f8bfd66b956aedc9957 (patch) | |
tree | b79c9bc2d4f9402dcd15d993b088840e2fad8a54 /models | |
parent | 9d3732dfd512273992855097bba1e909f098db23 (diff) | |
download | gitea-e777c6bdc6f12f9152335f8bfd66b956aedc9957.tar.gz gitea-e777c6bdc6f12f9152335f8bfd66b956aedc9957.zip |
Integrate OAuth2 Provider (#5378)
Diffstat (limited to 'models')
-rw-r--r-- | models/error.go | 39 | ||||
-rw-r--r-- | models/fixtures/oauth2_application.yml | 9 | ||||
-rw-r--r-- | models/fixtures/oauth2_authorization_code.yml | 8 | ||||
-rw-r--r-- | models/fixtures/oauth2_grant.yml | 6 | ||||
-rw-r--r-- | models/models.go | 3 | ||||
-rw-r--r-- | models/oauth2_application.go | 457 | ||||
-rw-r--r-- | models/oauth2_application_test.go | 209 | ||||
-rw-r--r-- | models/user.go | 1 |
8 files changed, 732 insertions, 0 deletions
diff --git a/models/error.go b/models/error.go index 649d9b87a8..6a135bda1a 100644 --- a/models/error.go +++ b/models/error.go @@ -1398,3 +1398,42 @@ func IsErrReviewNotExist(err error) bool { func (err ErrReviewNotExist) Error() string { return fmt.Sprintf("review does not exist [id: %d]", err.ID) } + +// ________ _____ __ .__ +// \_____ \ / _ \ __ ___/ |_| |__ +// / | \ / /_\ \| | \ __\ | \ +// / | \/ | \ | /| | | Y \ +// \_______ /\____|__ /____/ |__| |___| / +// \/ \/ \/ + +// ErrOAuthClientIDInvalid will be thrown if client id cannot be found +type ErrOAuthClientIDInvalid struct { + ClientID string +} + +// IsErrOauthClientIDInvalid checks if an error is a ErrReviewNotExist. +func IsErrOauthClientIDInvalid(err error) bool { + _, ok := err.(ErrOAuthClientIDInvalid) + return ok +} + +// Error returns the error message +func (err ErrOAuthClientIDInvalid) Error() string { + return fmt.Sprintf("Client ID invalid [Client ID: %s]", err.ClientID) +} + +// ErrOAuthApplicationNotFound will be thrown if id cannot be found +type ErrOAuthApplicationNotFound struct { + ID int64 +} + +// IsErrOAuthApplicationNotFound checks if an error is a ErrReviewNotExist. +func IsErrOAuthApplicationNotFound(err error) bool { + _, ok := err.(ErrOAuthApplicationNotFound) + return ok +} + +// Error returns the error message +func (err ErrOAuthApplicationNotFound) Error() string { + return fmt.Sprintf("OAuth application not found [ID: %d]", err.ID) +} diff --git a/models/fixtures/oauth2_application.yml b/models/fixtures/oauth2_application.yml new file mode 100644 index 0000000000..a13e20b10e --- /dev/null +++ b/models/fixtures/oauth2_application.yml @@ -0,0 +1,9 @@ +- + id: 1 + uid: 1 + name: "Test" + client_id: "da7da3ba-9a13-4167-856f-3899de0b0138" + client_secret: "$2a$10$UYRgUSgekzBp6hYe8pAdc.cgB4Gn06QRKsORUnIYTYQADs.YR/uvi" # bcrypt of "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA= + redirect_uris: '["a"]' + created_unix: 1546869730 + updated_unix: 1546869730 diff --git a/models/fixtures/oauth2_authorization_code.yml b/models/fixtures/oauth2_authorization_code.yml new file mode 100644 index 0000000000..2abce16354 --- /dev/null +++ b/models/fixtures/oauth2_authorization_code.yml @@ -0,0 +1,8 @@ +- id: 1 + grant_id: 1 + code: "authcode" + code_challenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg" # Code Verifier: N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt + code_challenge_method: "S256" + redirect_uri: "a" + valid_until: 3546869730 + diff --git a/models/fixtures/oauth2_grant.yml b/models/fixtures/oauth2_grant.yml new file mode 100644 index 0000000000..113eec7e5c --- /dev/null +++ b/models/fixtures/oauth2_grant.yml @@ -0,0 +1,6 @@ +- id: 1 + user_id: 1 + application_id: 1 + counter: 1 + created_unix: 1546869730 + updated_unix: 1546869730 diff --git a/models/models.go b/models/models.go index faa363e835..ac7e2e93ba 100644 --- a/models/models.go +++ b/models/models.go @@ -125,6 +125,9 @@ func init() { new(U2FRegistration), new(TeamUnit), new(Review), + new(OAuth2Application), + new(OAuth2AuthorizationCode), + new(OAuth2Grant), ) gonicNames := []string{"SSL", "UID"} diff --git a/models/oauth2_application.go b/models/oauth2_application.go new file mode 100644 index 0000000000..dd80a79b48 --- /dev/null +++ b/models/oauth2_application.go @@ -0,0 +1,457 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + "net/url" + "time" + + "github.com/go-xorm/xorm" + uuid "github.com/satori/go.uuid" + + "code.gitea.io/gitea/modules/secret" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + + "github.com/Unknwon/com" + "github.com/dgrijalva/jwt-go" + "golang.org/x/crypto/bcrypt" +) + +// OAuth2Application represents an OAuth2 client (RFC 6749) +type OAuth2Application struct { + ID int64 `xorm:"pk autoincr"` + UID int64 `xorm:"INDEX"` + User *User `xorm:"-"` + + Name string + + ClientID string `xorm:"unique"` + ClientSecret string + + RedirectURIs []string `xorm:"redirect_uris JSON TEXT"` + + CreatedUnix util.TimeStamp `xorm:"INDEX created"` + UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` +} + +// TableName sets the table name to `oauth2_application` +func (app *OAuth2Application) TableName() string { + return "oauth2_application" +} + +// PrimaryRedirectURI returns the first redirect uri or an empty string if empty +func (app *OAuth2Application) PrimaryRedirectURI() string { + if len(app.RedirectURIs) == 0 { + return "" + } + return app.RedirectURIs[0] +} + +// LoadUser will load User by UID +func (app *OAuth2Application) LoadUser() (err error) { + if app.User == nil { + app.User, err = GetUserByID(app.UID) + } + return +} + +// ContainsRedirectURI checks if redirectURI is allowed for app +func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool { + return com.IsSliceContainsStr(app.RedirectURIs, redirectURI) +} + +// GenerateClientSecret will generate the client secret and returns the plaintext and saves the hash at the database +func (app *OAuth2Application) GenerateClientSecret() (string, error) { + clientSecret, err := secret.New() + if err != nil { + return "", err + } + hashedSecret, err := bcrypt.GenerateFromPassword([]byte(clientSecret), bcrypt.DefaultCost) + if err != nil { + return "", err + } + app.ClientSecret = string(hashedSecret) + if _, err := x.ID(app.ID).Cols("client_secret").Update(app); err != nil { + return "", err + } + return clientSecret, nil +} + +// ValidateClientSecret validates the given secret by the hash saved in database +func (app *OAuth2Application) ValidateClientSecret(secret []byte) bool { + return bcrypt.CompareHashAndPassword([]byte(app.ClientSecret), secret) == nil +} + +// GetGrantByUserID returns a OAuth2Grant by its user and application ID +func (app *OAuth2Application) GetGrantByUserID(userID int64) (*OAuth2Grant, error) { + return app.getGrantByUserID(x, userID) +} + +func (app *OAuth2Application) getGrantByUserID(e Engine, userID int64) (grant *OAuth2Grant, err error) { + grant = new(OAuth2Grant) + if has, err := e.Where("user_id = ? AND application_id = ?", userID, app.ID).Get(grant); err != nil { + return nil, err + } else if !has { + return nil, nil + } + return grant, nil +} + +// CreateGrant generates a grant for an user +func (app *OAuth2Application) CreateGrant(userID int64) (*OAuth2Grant, error) { + return app.createGrant(x, userID) +} + +func (app *OAuth2Application) createGrant(e Engine, userID int64) (*OAuth2Grant, error) { + grant := &OAuth2Grant{ + ApplicationID: app.ID, + UserID: userID, + } + _, err := e.Insert(grant) + if err != nil { + return nil, err + } + return grant, nil +} + +// GetOAuth2ApplicationByClientID returns the oauth2 application with the given client_id. Returns an error if not found. +func GetOAuth2ApplicationByClientID(clientID string) (app *OAuth2Application, err error) { + return getOAuth2ApplicationByClientID(x, clientID) +} + +func getOAuth2ApplicationByClientID(e Engine, clientID string) (app *OAuth2Application, err error) { + app = new(OAuth2Application) + has, err := e.Where("client_id = ?", clientID).Get(app) + if !has { + return nil, ErrOAuthClientIDInvalid{ClientID: clientID} + } + return +} + +// GetOAuth2ApplicationByID returns the oauth2 application with the given id. Returns an error if not found. +func GetOAuth2ApplicationByID(id int64) (app *OAuth2Application, err error) { + return getOAuth2ApplicationByID(x, id) +} + +func getOAuth2ApplicationByID(e Engine, id int64) (app *OAuth2Application, err error) { + app = new(OAuth2Application) + has, err := e.ID(id).Get(app) + if !has { + return nil, ErrOAuthApplicationNotFound{ID: id} + } + return app, nil +} + +// GetOAuth2ApplicationsByUserID returns all oauth2 applications owned by the user +func GetOAuth2ApplicationsByUserID(userID int64) (apps []*OAuth2Application, err error) { + return getOAuth2ApplicationsByUserID(x, userID) +} + +func getOAuth2ApplicationsByUserID(e Engine, userID int64) (apps []*OAuth2Application, err error) { + apps = make([]*OAuth2Application, 0) + err = e.Where("uid = ?", userID).Find(&apps) + return +} + +// CreateOAuth2ApplicationOptions holds options to create an oauth2 application +type CreateOAuth2ApplicationOptions struct { + Name string + UserID int64 + RedirectURIs []string +} + +// CreateOAuth2Application inserts a new oauth2 application +func CreateOAuth2Application(opts CreateOAuth2ApplicationOptions) (*OAuth2Application, error) { + return createOAuth2Application(x, opts) +} + +func createOAuth2Application(e Engine, opts CreateOAuth2ApplicationOptions) (*OAuth2Application, error) { + clientID := uuid.NewV4().String() + app := &OAuth2Application{ + UID: opts.UserID, + Name: opts.Name, + ClientID: clientID, + RedirectURIs: opts.RedirectURIs, + } + if _, err := e.Insert(app); err != nil { + return nil, err + } + return app, nil +} + +// UpdateOAuth2ApplicationOptions holds options to update an oauth2 application +type UpdateOAuth2ApplicationOptions struct { + ID int64 + Name string + UserID int64 + RedirectURIs []string +} + +// UpdateOAuth2Application updates an oauth2 application +func UpdateOAuth2Application(opts UpdateOAuth2ApplicationOptions) error { + return updateOAuth2Application(x, opts) +} + +func updateOAuth2Application(e Engine, opts UpdateOAuth2ApplicationOptions) error { + app := &OAuth2Application{ + ID: opts.ID, + UID: opts.UserID, + Name: opts.Name, + RedirectURIs: opts.RedirectURIs, + } + if _, err := e.ID(opts.ID).Update(app); err != nil { + return err + } + return nil +} + +func deleteOAuth2Application(sess *xorm.Session, id, userid int64) error { + if deleted, err := sess.Delete(&OAuth2Application{ID: id, UID: userid}); err != nil { + return err + } else if deleted == 0 { + return fmt.Errorf("cannot find oauth2 application") + } + codes := make([]*OAuth2AuthorizationCode, 0) + // delete correlating auth codes + if err := sess.Join("INNER", "oauth2_grant", + "oauth2_authorization_code.grant_id = oauth2_grant.id AND oauth2_grant.application_id = ?", id).Find(&codes); err != nil { + return err + } + codeIDs := make([]int64, 0) + for _, grant := range codes { + codeIDs = append(codeIDs, grant.ID) + } + + if _, err := sess.In("id", codeIDs).Delete(new(OAuth2AuthorizationCode)); err != nil { + return err + } + + if _, err := sess.Where("application_id = ?", id).Delete(new(OAuth2Grant)); err != nil { + return err + } + return nil +} + +// DeleteOAuth2Application deletes the application with the given id and the grants and auth codes related to it. It checks if the userid was the creator of the app. +func DeleteOAuth2Application(id, userid int64) error { + sess := x.NewSession() + if err := sess.Begin(); err != nil { + return err + } + if err := deleteOAuth2Application(sess, id, userid); err != nil { + return err + } + return sess.Commit() +} + +////////////////////////////////////////////////////// + +// OAuth2AuthorizationCode is a code to obtain an access token in combination with the client secret once. It has a limited lifetime. +type OAuth2AuthorizationCode struct { + ID int64 `xorm:"pk autoincr"` + Grant *OAuth2Grant `xorm:"-"` + GrantID int64 + Code string `xorm:"INDEX unique"` + CodeChallenge string + CodeChallengeMethod string + RedirectURI string + ValidUntil util.TimeStamp `xorm:"index"` +} + +// TableName sets the table name to `oauth2_authorization_code` +func (code *OAuth2AuthorizationCode) TableName() string { + return "oauth2_authorization_code" +} + +// GenerateRedirectURI generates a redirect URI for a successful authorization request. State will be used if not empty. +func (code *OAuth2AuthorizationCode) GenerateRedirectURI(state string) (redirect *url.URL, err error) { + if redirect, err = url.Parse(code.RedirectURI); err != nil { + return + } + q := redirect.Query() + if state != "" { + q.Set("state", state) + } + q.Set("code", code.Code) + redirect.RawQuery = q.Encode() + return +} + +// Invalidate deletes the auth code from the database to invalidate this code +func (code *OAuth2AuthorizationCode) Invalidate() error { + return code.invalidate(x) +} + +func (code *OAuth2AuthorizationCode) invalidate(e Engine) error { + _, err := e.Delete(code) + return err +} + +// ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE implementation. +func (code *OAuth2AuthorizationCode) ValidateCodeChallenge(verifier string) bool { + return code.validateCodeChallenge(x, verifier) +} + +func (code *OAuth2AuthorizationCode) validateCodeChallenge(e Engine, verifier string) bool { + switch code.CodeChallengeMethod { + case "S256": + // base64url(SHA256(verifier)) see https://tools.ietf.org/html/rfc7636#section-4.6 + h := sha256.Sum256([]byte(verifier)) + hashedVerifier := base64.RawURLEncoding.EncodeToString(h[:]) + return hashedVerifier == code.CodeChallenge + case "plain": + return verifier == code.CodeChallenge + case "": + return true + default: + // unsupported method -> return false + return false + } +} + +// GetOAuth2AuthorizationByCode returns an authorization by its code +func GetOAuth2AuthorizationByCode(code string) (*OAuth2AuthorizationCode, error) { + return getOAuth2AuthorizationByCode(x, code) +} + +func getOAuth2AuthorizationByCode(e Engine, code string) (auth *OAuth2AuthorizationCode, err error) { + auth = new(OAuth2AuthorizationCode) + if has, err := e.Where("code = ?", code).Get(auth); err != nil { + return nil, err + } else if !has { + return nil, nil + } + auth.Grant = new(OAuth2Grant) + if has, err := e.ID(auth.GrantID).Get(auth.Grant); err != nil { + return nil, err + } else if !has { + return nil, nil + } + return auth, nil +} + +////////////////////////////////////////////////////// + +// OAuth2Grant represents the permission of an user for a specifc application to access resources +type OAuth2Grant struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"INDEX unique(user_application)"` + ApplicationID int64 `xorm:"INDEX unique(user_application)"` + Counter int64 `xorm:"NOT NULL DEFAULT 1"` + CreatedUnix util.TimeStamp `xorm:"created"` + UpdatedUnix util.TimeStamp `xorm:"updated"` +} + +// TableName sets the table name to `oauth2_grant` +func (grant *OAuth2Grant) TableName() string { + return "oauth2_grant" +} + +// GenerateNewAuthorizationCode generates a new authorization code for a grant and saves it to the databse +func (grant *OAuth2Grant) GenerateNewAuthorizationCode(redirectURI, codeChallenge, codeChallengeMethod string) (*OAuth2AuthorizationCode, error) { + return grant.generateNewAuthorizationCode(x, redirectURI, codeChallenge, codeChallengeMethod) +} + +func (grant *OAuth2Grant) generateNewAuthorizationCode(e Engine, redirectURI, codeChallenge, codeChallengeMethod string) (code *OAuth2AuthorizationCode, err error) { + var codeSecret string + if codeSecret, err = secret.New(); err != nil { + return &OAuth2AuthorizationCode{}, err + } + code = &OAuth2AuthorizationCode{ + Grant: grant, + GrantID: grant.ID, + RedirectURI: redirectURI, + Code: codeSecret, + CodeChallenge: codeChallenge, + CodeChallengeMethod: codeChallengeMethod, + } + if _, err := e.Insert(code); err != nil { + return nil, err + } + return code, nil +} + +// IncreaseCounter increases the counter and updates the grant +func (grant *OAuth2Grant) IncreaseCounter() error { + return grant.increaseCount(x) +} + +func (grant *OAuth2Grant) increaseCount(e Engine) error { + _, err := e.ID(grant.ID).Incr("counter").Update(new(OAuth2Grant)) + if err != nil { + return err + } + updatedGrant, err := getOAuth2GrantByID(e, grant.ID) + if err != nil { + return err + } + grant.Counter = updatedGrant.Counter + return nil +} + +// GetOAuth2GrantByID returns the grant with the given ID +func GetOAuth2GrantByID(id int64) (*OAuth2Grant, error) { + return getOAuth2GrantByID(x, id) +} + +func getOAuth2GrantByID(e Engine, id int64) (grant *OAuth2Grant, err error) { + grant = new(OAuth2Grant) + if has, err := e.ID(id).Get(grant); err != nil { + return nil, err + } else if !has { + return nil, nil + } + return +} + +////////////////////////////////////////////////////////////// + +// OAuth2TokenType represents the type of token for an oauth application +type OAuth2TokenType int + +const ( + // TypeAccessToken is a token with short lifetime to access the api + TypeAccessToken OAuth2TokenType = 0 + // TypeRefreshToken is token with long lifetime to refresh access tokens obtained by the client + TypeRefreshToken = iota +) + +// OAuth2Token represents a JWT token used to authenticate a client +type OAuth2Token struct { + GrantID int64 `json:"gnt"` + Type OAuth2TokenType `json:"tt"` + Counter int64 `json:"cnt,omitempty"` + jwt.StandardClaims +} + +// ParseOAuth2Token parses a singed jwt string +func ParseOAuth2Token(jwtToken string) (*OAuth2Token, error) { + parsedToken, err := jwt.ParseWithClaims(jwtToken, &OAuth2Token{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing algo: %v", token.Header["alg"]) + } + return setting.OAuth2.JWTSecretBytes, nil + }) + if err != nil { + return nil, err + } + var token *OAuth2Token + var ok bool + if token, ok = parsedToken.Claims.(*OAuth2Token); !ok || !parsedToken.Valid { + return nil, fmt.Errorf("invalid token") + } + return token, nil +} + +// SignToken signs the token with the JWT secret +func (token *OAuth2Token) SignToken() (string, error) { + token.IssuedAt = time.Now().Unix() + jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS512, token) + return jwtToken.SignedString(setting.OAuth2.JWTSecretBytes) +} diff --git a/models/oauth2_application_test.go b/models/oauth2_application_test.go new file mode 100644 index 0000000000..b06d9356c0 --- /dev/null +++ b/models/oauth2_application_test.go @@ -0,0 +1,209 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +//////////////////// Application + +func TestOAuth2Application_GenerateClientSecret(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + app := AssertExistsAndLoadBean(t, &OAuth2Application{ID: 1}).(*OAuth2Application) + secret, err := app.GenerateClientSecret() + assert.NoError(t, err) + assert.True(t, len(secret) > 0) + AssertExistsAndLoadBean(t, &OAuth2Application{ID: 1, ClientSecret: app.ClientSecret}) +} + +func BenchmarkOAuth2Application_GenerateClientSecret(b *testing.B) { + assert.NoError(b, PrepareTestDatabase()) + app := AssertExistsAndLoadBean(b, &OAuth2Application{ID: 1}).(*OAuth2Application) + for i := 0; i < b.N; i++ { + _, _ = app.GenerateClientSecret() + } +} + +func TestOAuth2Application_ContainsRedirectURI(t *testing.T) { + app := &OAuth2Application{ + RedirectURIs: []string{"a", "b", "c"}, + } + assert.True(t, app.ContainsRedirectURI("a")) + assert.True(t, app.ContainsRedirectURI("b")) + assert.True(t, app.ContainsRedirectURI("c")) + assert.False(t, app.ContainsRedirectURI("d")) +} + +func TestOAuth2Application_ValidateClientSecret(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + app := AssertExistsAndLoadBean(t, &OAuth2Application{ID: 1}).(*OAuth2Application) + secret, err := app.GenerateClientSecret() + assert.NoError(t, err) + assert.True(t, app.ValidateClientSecret([]byte(secret))) + assert.False(t, app.ValidateClientSecret([]byte("fewijfowejgfiowjeoifew"))) +} + +func TestGetOAuth2ApplicationByClientID(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + app, err := GetOAuth2ApplicationByClientID("da7da3ba-9a13-4167-856f-3899de0b0138") + assert.NoError(t, err) + assert.Equal(t, "da7da3ba-9a13-4167-856f-3899de0b0138", app.ClientID) + + app, err = GetOAuth2ApplicationByClientID("invalid client id") + assert.Error(t, err) + assert.Nil(t, app) +} + +func TestCreateOAuth2Application(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + app, err := CreateOAuth2Application(CreateOAuth2ApplicationOptions{Name: "newapp", UserID: 1}) + assert.NoError(t, err) + assert.Equal(t, "newapp", app.Name) + assert.Len(t, app.ClientID, 36) + AssertExistsAndLoadBean(t, &OAuth2Application{Name: "newapp"}) +} + +func TestOAuth2Application_LoadUser(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + app := AssertExistsAndLoadBean(t, &OAuth2Application{ID: 1}).(*OAuth2Application) + assert.NoError(t, app.LoadUser()) + assert.NotNil(t, app.User) +} + +func TestOAuth2Application_TableName(t *testing.T) { + assert.Equal(t, "oauth2_application", new(OAuth2Application).TableName()) +} + +func TestOAuth2Application_GetGrantByUserID(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + app := AssertExistsAndLoadBean(t, &OAuth2Application{ID: 1}).(*OAuth2Application) + grant, err := app.GetGrantByUserID(1) + assert.NoError(t, err) + assert.Equal(t, int64(1), grant.UserID) + + grant, err = app.GetGrantByUserID(34923458) + assert.NoError(t, err) + assert.Nil(t, grant) +} + +func TestOAuth2Application_CreateGrant(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + app := AssertExistsAndLoadBean(t, &OAuth2Application{ID: 1}).(*OAuth2Application) + grant, err := app.CreateGrant(2) + assert.NoError(t, err) + assert.NotNil(t, grant) + assert.Equal(t, int64(2), grant.UserID) + assert.Equal(t, int64(1), grant.ApplicationID) +} + +//////////////////// Grant + +func TestGetOAuth2GrantByID(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + grant, err := GetOAuth2GrantByID(1) + assert.NoError(t, err) + assert.Equal(t, int64(1), grant.ID) + + grant, err = GetOAuth2GrantByID(34923458) + assert.NoError(t, err) + assert.Nil(t, grant) +} + +func TestOAuth2Grant_IncreaseCounter(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + grant := AssertExistsAndLoadBean(t, &OAuth2Grant{ID: 1, Counter: 1}).(*OAuth2Grant) + assert.NoError(t, grant.IncreaseCounter()) + assert.Equal(t, int64(2), grant.Counter) + AssertExistsAndLoadBean(t, &OAuth2Grant{ID: 1, Counter: 2}) +} + +func TestOAuth2Grant_GenerateNewAuthorizationCode(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + grant := AssertExistsAndLoadBean(t, &OAuth2Grant{ID: 1}).(*OAuth2Grant) + code, err := grant.GenerateNewAuthorizationCode("https://example2.com/callback", "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", "S256") + assert.NoError(t, err) + assert.NotNil(t, code) + assert.True(t, len(code.Code) > 32) // secret length > 32 +} + +func TestOAuth2Grant_TableName(t *testing.T) { + assert.Equal(t, "oauth2_grant", new(OAuth2Grant).TableName()) +} + +//////////////////// Authorization Code + +func TestGetOAuth2AuthorizationByCode(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + code, err := GetOAuth2AuthorizationByCode("authcode") + assert.NoError(t, err) + assert.NotNil(t, code) + assert.Equal(t, "authcode", code.Code) + assert.Equal(t, int64(1), code.ID) + + code, err = GetOAuth2AuthorizationByCode("does not exist") + assert.NoError(t, err) + assert.Nil(t, code) +} + +func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) { + // test plain + code := &OAuth2AuthorizationCode{ + CodeChallengeMethod: "plain", + CodeChallenge: "test123", + } + assert.True(t, code.ValidateCodeChallenge("test123")) + assert.False(t, code.ValidateCodeChallenge("ierwgjoergjio")) + + // test S256 + code = &OAuth2AuthorizationCode{ + CodeChallengeMethod: "S256", + CodeChallenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", + } + assert.True(t, code.ValidateCodeChallenge("N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt")) + assert.False(t, code.ValidateCodeChallenge("wiogjerogorewngoenrgoiuenorg")) + + // test unknown + code = &OAuth2AuthorizationCode{ + CodeChallengeMethod: "monkey", + CodeChallenge: "foiwgjioriogeiogjerger", + } + assert.False(t, code.ValidateCodeChallenge("foiwgjioriogeiogjerger")) + + // test no code challenge + code = &OAuth2AuthorizationCode{ + CodeChallengeMethod: "", + CodeChallenge: "foierjiogerogerg", + } + assert.True(t, code.ValidateCodeChallenge("")) +} + +func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) { + code := &OAuth2AuthorizationCode{ + RedirectURI: "https://example.com/callback", + Code: "thecode", + } + + redirect, err := code.GenerateRedirectURI("thestate") + assert.NoError(t, err) + assert.Equal(t, redirect.String(), "https://example.com/callback?code=thecode&state=thestate") + + redirect, err = code.GenerateRedirectURI("") + assert.NoError(t, err) + assert.Equal(t, redirect.String(), "https://example.com/callback?code=thecode") +} + +func TestOAuth2AuthorizationCode_Invalidate(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + code := AssertExistsAndLoadBean(t, &OAuth2AuthorizationCode{Code: "authcode"}).(*OAuth2AuthorizationCode) + assert.NoError(t, code.Invalidate()) + AssertNotExistsBean(t, &OAuth2AuthorizationCode{Code: "authcode"}) +} + +func TestOAuth2AuthorizationCode_TableName(t *testing.T) { + assert.Equal(t, "oauth2_authorization_code", new(OAuth2AuthorizationCode).TableName()) +} diff --git a/models/user.go b/models/user.go index b244f4ecef..1ed949355f 100644 --- a/models/user.go +++ b/models/user.go @@ -742,6 +742,7 @@ var ( "template", "user", "vendor", + "login", "robots.txt", ".", "..", |