Integrate OAuth2 Provider (#5378)
parent
9d3732dfd5
commit
e777c6bdc6
@ -0,0 +1,138 @@
|
||||
// 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 integrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const defaultAuthorize = "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate"
|
||||
|
||||
func TestNoClientID(t *testing.T) {
|
||||
prepareTestEnv(t)
|
||||
req := NewRequest(t, "GET", "/login/oauth/authorize")
|
||||
ctx := loginUser(t, "user2")
|
||||
ctx.MakeRequest(t, req, 400)
|
||||
}
|
||||
|
||||
func TestLoginRedirect(t *testing.T) {
|
||||
prepareTestEnv(t)
|
||||
req := NewRequest(t, "GET", "/login/oauth/authorize")
|
||||
assert.Contains(t, MakeRequest(t, req, 302).Body.String(), "/user/login")
|
||||
}
|
||||
|
||||
func TestShowAuthorize(t *testing.T) {
|
||||
prepareTestEnv(t)
|
||||
req := NewRequest(t, "GET", defaultAuthorize)
|
||||
ctx := loginUser(t, "user4")
|
||||
resp := ctx.MakeRequest(t, req, 200)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
htmlDoc.AssertElement(t, "#authorize-app", true)
|
||||
htmlDoc.GetCSRF()
|
||||
}
|
||||
|
||||
func TestRedirectWithExistingGrant(t *testing.T) {
|
||||
prepareTestEnv(t)
|
||||
req := NewRequest(t, "GET", defaultAuthorize)
|
||||
ctx := loginUser(t, "user1")
|
||||
resp := ctx.MakeRequest(t, req, 302)
|
||||
u, err := resp.Result().Location()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "thestate", u.Query().Get("state"))
|
||||
assert.Truef(t, len(u.Query().Get("code")) > 30, "authorization code '%s' should be longer then 30", u.Query().Get("code"))
|
||||
}
|
||||
|
||||
func TestAccessTokenExchange(t *testing.T) {
|
||||
prepareTestEnv(t)
|
||||
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
|
||||
"redirect_uri": "a",
|
||||
"code": "authcode",
|
||||
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
|
||||
})
|
||||
resp := MakeRequest(t, req, 200)
|
||||
type response struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
parsed := new(response)
|
||||
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
|
||||
assert.True(t, len(parsed.AccessToken) > 10)
|
||||
assert.True(t, len(parsed.RefreshToken) > 10)
|
||||
}
|
||||
|
||||
func TestAccessTokenExchangeWithoutPKCE(t *testing.T) {
|
||||
prepareTestEnv(t)
|
||||
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
|
||||
"redirect_uri": "a",
|
||||
"code": "authcode",
|
||||
})
|
||||
MakeRequest(t, req, 400)
|
||||
}
|
||||
|
||||
func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
|
||||
prepareTestEnv(t)
|
||||
// invalid client id
|
||||
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": "???",
|
||||
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
|
||||
"redirect_uri": "a",
|
||||
"code": "authcode",
|
||||
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
|
||||
})
|
||||
MakeRequest(t, req, 400)
|
||||
// invalid client secret
|
||||
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||
"client_secret": "???",
|
||||
"redirect_uri": "a",
|
||||
"code": "authcode",
|
||||
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
|
||||
})
|
||||
MakeRequest(t, req, 400)
|
||||
// invalid redirect uri
|
||||
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
|
||||
"redirect_uri": "???",
|
||||
"code": "authcode",
|
||||
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
|
||||
})
|
||||
MakeRequest(t, req, 400)
|
||||
// invalid authorization code
|
||||
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
|
||||
"redirect_uri": "a",
|
||||
"code": "???",
|
||||
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
|
||||
})
|
||||
MakeRequest(t, req, 400)
|
||||
// invalid grant_type
|
||||
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "???",
|
||||
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
|
||||
"redirect_uri": "a",
|
||||
"code": "authcode",
|
||||
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
|
||||
})
|
||||
MakeRequest(t, req, 400)
|
||||
}
|
@ -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
|
@ -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
|
||||
|
@ -0,0 +1,6 @@
|
||||
- id: 1
|
||||
user_id: 1
|
||||
application_id: 1
|
||||
counter: 1
|
||||
created_unix: 1546869730
|
||||
updated_unix: 1546869730
|
@ -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)
|
||||
}
|
@ -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())
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
// 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 secret
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
// New creats a new secret
|
||||
func New() (string, error) {
|
||||
return NewWithLength(32)
|
||||
}
|
||||
|
||||
// NewWithLength creates a new secret for a given length
|
||||
func NewWithLength(length int64) (string, error) {
|
||||
return randomString(length)
|
||||
}
|
||||
|
||||
func randomBytes(len int64) ([]byte, error) {
|
||||
b := make([]byte, len)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func randomString(len int64) (string, error) {
|
||||
b, err := randomBytes(len)
|
||||
return base64.URLEncoding.EncodeToString(b), err
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
// 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 secret
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
result, err := New()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, len(result) > 32)
|
||||
|
||||
result2, err := New()
|
||||
assert.NoError(t, err)
|
||||
// check if secrets
|
||||
assert.NotEqual(t, result, result2)
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,452 @@
|
||||
// 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 user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/go-macaron/binding"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/auth"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
const (
|
||||
tplGrantAccess base.TplName = "user/auth/grant"
|
||||
tplGrantError base.TplName = "user/auth/grant_error"
|
||||
)
|
||||
|
||||
// TODO move error and responses to SDK or models
|
||||
|
||||
// AuthorizeErrorCode represents an error code specified in RFC 6749
|
||||
type AuthorizeErrorCode string
|
||||
|
||||
const (
|
||||