summaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
authorJonas Franz <info@jonasfranz.software>2019-03-08 17:42:50 +0100
committertechknowlogick <matti@mdranta.net>2019-03-08 11:42:50 -0500
commite777c6bdc6f12f9152335f8bfd66b956aedc9957 (patch)
treeb79c9bc2d4f9402dcd15d993b088840e2fad8a54 /models
parent9d3732dfd512273992855097bba1e909f098db23 (diff)
downloadgitea-e777c6bdc6f12f9152335f8bfd66b956aedc9957.tar.gz
gitea-e777c6bdc6f12f9152335f8bfd66b956aedc9957.zip
Integrate OAuth2 Provider (#5378)
Diffstat (limited to 'models')
-rw-r--r--models/error.go39
-rw-r--r--models/fixtures/oauth2_application.yml9
-rw-r--r--models/fixtures/oauth2_authorization_code.yml8
-rw-r--r--models/fixtures/oauth2_grant.yml6
-rw-r--r--models/models.go3
-rw-r--r--models/oauth2_application.go457
-rw-r--r--models/oauth2_application_test.go209
-rw-r--r--models/user.go1
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",
".",
"..",