Integrate OAuth2 Provider (#5378)
This commit is contained in:

committed by
techknowlogick

parent
9d3732dfd5
commit
e777c6bdc6
5
Gopkg.lock
generated
5
Gopkg.lock
generated
@ -962,11 +962,13 @@
|
||||
revision = "2c050d2dae5345c417db301f11fda6fbf5ad0f0a"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c3d6b9e2cf3936ba9927da2e8858651aad69890b9dd3349f1316b4003b25d7a3"
|
||||
digest = "1:590035a7bbea1c037c2e6d51f81ee09857480c3c21f1f77397d9802f10507c06"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"acme",
|
||||
"acme/autocert",
|
||||
"bcrypt",
|
||||
"blowfish",
|
||||
"cast5",
|
||||
"curve25519",
|
||||
"ed25519",
|
||||
@ -1315,6 +1317,7 @@
|
||||
"github.com/urfave/cli",
|
||||
"github.com/yohcop/openid-go",
|
||||
"golang.org/x/crypto/acme/autocert",
|
||||
"golang.org/x/crypto/bcrypt",
|
||||
"golang.org/x/crypto/pbkdf2",
|
||||
"golang.org/x/crypto/ssh",
|
||||
"golang.org/x/net/html",
|
||||
|
@ -63,7 +63,7 @@ func runGenerateInternalToken(c *cli.Context) error {
|
||||
}
|
||||
|
||||
func runGenerateLfsJwtSecret(c *cli.Context) error {
|
||||
JWTSecretBase64, err := generate.NewLfsJwtSecret()
|
||||
JWTSecretBase64, err := generate.NewJwtSecret()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -654,6 +654,16 @@ DEFAULT_PAGING_NUM = 30
|
||||
; Default and maximum number of items per page for git trees api
|
||||
DEFAULT_GIT_TREES_PER_PAGE = 1000
|
||||
|
||||
[oauth2]
|
||||
; Enables OAuth2 provider
|
||||
ENABLED = true
|
||||
; Lifetime of an OAuth2 access token in seconds
|
||||
ACCESS_TOKEN_EXPIRATION_TIME=3600
|
||||
; Lifetime of an OAuth2 access token in hours
|
||||
REFRESH_TOKEN_EXPIRATION_TIME=730
|
||||
; OAuth2 authentication secret for access and refresh tokens, change this a unique string.
|
||||
JWT_SECRET=Bk0yK7Y9g_p56v86KaHqjSbxvNvu3SbKoOdOt2ZcXvU
|
||||
|
||||
[i18n]
|
||||
LANGS = en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,uk-UA,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR
|
||||
NAMES = English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,Українська,日本語,español,português do Brasil,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어
|
||||
|
@ -345,6 +345,13 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||
- `DEFAULT_PAGING_NUM`: **30**: Default paging number of api.
|
||||
- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: Default and maximum number of items per page for git trees api.
|
||||
|
||||
## OAuth2 (`oauth2`)
|
||||
|
||||
- `ENABLED`: **true**: Enables OAuth2 provider.
|
||||
- `ACCESS_TOKEN_EXPIRATION_TIME`: **3600**: Lifetime of an OAuth2 access token in seconds
|
||||
- `REFRESH_TOKEN_EXPIRATION_TIME`: **730**: Lifetime of an OAuth2 access token in hours
|
||||
- `JWT_SECRET`: **\<empty\>**: OAuth2 authentication secret for access and refresh tokens, change this a unique string.
|
||||
|
||||
## i18n (`i18n`)
|
||||
|
||||
- `LANGS`: **en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR**: List of locales shown in language selector
|
||||
|
138
integrations/oauth_test.go
Normal file
138
integrations/oauth_test.go
Normal file
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
9
models/fixtures/oauth2_application.yml
Normal file
9
models/fixtures/oauth2_application.yml
Normal file
@ -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
|
8
models/fixtures/oauth2_authorization_code.yml
Normal file
8
models/fixtures/oauth2_authorization_code.yml
Normal file
@ -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
|
||||
|
6
models/fixtures/oauth2_grant.yml
Normal file
6
models/fixtures/oauth2_grant.yml
Normal file
@ -0,0 +1,6 @@
|
||||
- id: 1
|
||||
user_id: 1
|
||||
application_id: 1
|
||||
counter: 1
|
||||
created_unix: 1546869730
|
||||
updated_unix: 1546869730
|
@ -125,6 +125,9 @@ func init() {
|
||||
new(U2FRegistration),
|
||||
new(TeamUnit),
|
||||
new(Review),
|
||||
new(OAuth2Application),
|
||||
new(OAuth2AuthorizationCode),
|
||||
new(OAuth2Grant),
|
||||
)
|
||||
|
||||
gonicNames := []string{"SSL", "UID"}
|
||||
|
457
models/oauth2_application.go
Normal file
457
models/oauth2_application.go
Normal file
File diff suppressed because it is too large
Load Diff
209
models/oauth2_application_test.go
Normal file
209
models/oauth2_application_test.go
Normal file
@ -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())
|
||||
}
|
@ -742,6 +742,7 @@ var (
|
||||
"template",
|
||||
"user",
|
||||
"vendor",
|
||||
"login",
|
||||
"robots.txt",
|
||||
".",
|
||||
"..",
|
||||
|
@ -7,6 +7,7 @@ package auth
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Unknwon/com"
|
||||
"github.com/go-macaron/binding"
|
||||
@ -44,7 +45,7 @@ func SignedInID(ctx *macaron.Context, sess session.Store) int64 {
|
||||
auHead := ctx.Req.Header.Get("Authorization")
|
||||
if len(auHead) > 0 {
|
||||
auths := strings.Fields(auHead)
|
||||
if len(auths) == 2 && auths[0] == "token" {
|
||||
if len(auths) == 2 && (auths[0] == "token" || strings.ToLower(auths[0]) == "bearer") {
|
||||
tokenSHA = auths[1]
|
||||
}
|
||||
}
|
||||
@ -52,6 +53,13 @@ func SignedInID(ctx *macaron.Context, sess session.Store) int64 {
|
||||
|
||||
// Let's see if token is valid.
|
||||
if len(tokenSHA) > 0 {
|
||||
if strings.Contains(tokenSHA, ".") {
|
||||
uid := checkOAuthAccessToken(tokenSHA)
|
||||
if uid != 0 {
|
||||
ctx.Data["IsApiToken"] = true
|
||||
}
|
||||
return uid
|
||||
}
|
||||
t, err := models.GetAccessTokenBySHA(tokenSHA)
|
||||
if err != nil {
|
||||
if models.IsErrAccessTokenNotExist(err) || models.IsErrAccessTokenEmpty(err) {
|
||||
@ -77,6 +85,29 @@ func SignedInID(ctx *macaron.Context, sess session.Store) int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func checkOAuthAccessToken(accessToken string) int64 {
|
||||
// JWT tokens require a "."
|
||||
if !strings.Contains(accessToken, ".") {
|
||||
return 0
|
||||
}
|
||||
token, err := models.ParseOAuth2Token(accessToken)
|
||||
if err != nil {
|
||||
log.Trace("ParseOAuth2Token", err)
|
||||
return 0
|
||||
}
|
||||
var grant *models.OAuth2Grant
|
||||
if grant, err = models.GetOAuth2GrantByID(token.GrantID); err != nil || grant == nil {
|
||||
return 0
|
||||
}
|
||||
if token.Type != models.TypeAccessToken {
|
||||
return 0
|
||||
}
|
||||
if token.ExpiresAt < time.Now().Unix() || token.IssuedAt > time.Now().Unix() {
|
||||
return 0
|
||||
}
|
||||
return grant.UserID
|
||||
}
|
||||
|
||||
// SignedInUser returns the user object of signed user.
|
||||
// It returns a bool value to indicate whether user uses basic auth or not.
|
||||
func SignedInUser(ctx *macaron.Context, sess session.Store) (*models.User, bool) {
|
||||
|
@ -137,6 +137,54 @@ func (f *SignInForm) Validate(ctx *macaron.Context, errs binding.Errors) binding
|
||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// AuthorizationForm form for authorizing oauth2 clients
|
||||
type AuthorizationForm struct {
|
||||
ResponseType string `binding:"Required;In(code)"`
|
||||
ClientID string `binding:"Required"`
|
||||
RedirectURI string
|
||||
State string
|
||||
|
||||
// PKCE support
|
||||
CodeChallengeMethod string // S256, plain
|
||||
CodeChallenge string
|
||||
}
|
||||
|
||||
// Validate valideates the fields
|
||||
func (f *AuthorizationForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// GrantApplicationForm form for authorizing oauth2 clients
|
||||
type GrantApplicationForm struct {
|
||||
ClientID string `binding:"Required"`
|
||||
RedirectURI string
|
||||
State string
|
||||
}
|
||||
|
||||
// Validate valideates the fields
|
||||
func (f *GrantApplicationForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// AccessTokenForm for issuing access tokens from authorization codes or refresh tokens
|
||||
type AccessTokenForm struct {
|
||||
GrantType string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
RedirectURI string
|
||||
// TODO Specify authentication code length to prevent against birthday attacks
|
||||
Code string
|
||||
RefreshToken string
|
||||
|
||||
// PKCE support
|
||||
CodeVerifier string
|
||||
}
|
||||
|
||||
// Validate valideates the fields
|
||||
func (f *AccessTokenForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// __________________________________________.___ _______ ________ _________
|
||||
// / _____/\_ _____/\__ ___/\__ ___/| |\ \ / _____/ / _____/
|
||||
// \_____ \ | __)_ | | | | | |/ | \/ \ ___ \_____ \
|
||||
@ -258,6 +306,17 @@ func (f *NewAccessTokenForm) Validate(ctx *macaron.Context, errs binding.Errors)
|
||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// EditOAuth2ApplicationForm form for editing oauth2 applications
|
||||
type EditOAuth2ApplicationForm struct {
|
||||
Name string `binding:"Required;MaxSize(255)" form:"application_name"`
|
||||
RedirectURI string `binding:"Required" form:"redirect_uri"`
|
||||
}
|
||||
|
||||
// Validate valideates the fields
|
||||
func (f *EditOAuth2ApplicationForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// TwoFactorAuthForm for logging in with 2FA token.
|
||||
type TwoFactorAuthForm struct {
|
||||
Passcode string `binding:"Required"`
|
||||
|
@ -57,16 +57,14 @@ func NewInternalToken() (string, error) {
|
||||
return internalToken, nil
|
||||
}
|
||||
|
||||
// NewLfsJwtSecret generate a new value intended to be used by LFS_JWT_SECRET.
|
||||
func NewLfsJwtSecret() (string, error) {
|
||||
// NewJwtSecret generate a new value intended to be used by LFS_JWT_SECRET.
|
||||
func NewJwtSecret() (string, error) {
|
||||
JWTSecretBytes := make([]byte, 32)
|
||||
_, err := io.ReadFull(rand.Reader, JWTSecretBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
JWTSecretBase64 := base64.RawURLEncoding.EncodeToString(JWTSecretBytes)
|
||||
return JWTSecretBase64, nil
|
||||
return base64.RawURLEncoding.EncodeToString(JWTSecretBytes), nil
|
||||
}
|
||||
|
||||
// NewSecretKey generate a new value intended to be used by SECRET_KEY.
|
||||
|
33
modules/secret/secret.go
Normal file
33
modules/secret/secret.go
Normal file
@ -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
|
||||
}
|
22
modules/secret/secret_test.go
Normal file
22
modules/secret/secret_test.go
Normal file
@ -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)
|
||||
}
|
@ -560,6 +560,18 @@ var (
|
||||
DefaultGitTreesPerPage: 1000,
|
||||
}
|
||||
|
||||
OAuth2 = struct {
|
||||
Enable bool
|
||||
AccessTokenExpirationTime int64
|
||||
RefreshTokenExpirationTime int64
|
||||
JWTSecretBytes []byte `ini:"-"`
|
||||
JWTSecretBase64 string `ini:"JWT_SECRET"`
|
||||
}{
|
||||
Enable: true,
|
||||
AccessTokenExpirationTime: 3600,
|
||||
RefreshTokenExpirationTime: 730,
|
||||
}
|
||||
|
||||
U2F = struct {
|
||||
AppID string
|
||||
TrustedFacets []string
|
||||
@ -922,7 +934,7 @@ func NewContext() {
|
||||
n, err := base64.RawURLEncoding.Decode(LFS.JWTSecretBytes, []byte(LFS.JWTSecretBase64))
|
||||
|
||||
if err != nil || n != 32 {
|
||||
LFS.JWTSecretBase64, err = generate.NewLfsJwtSecret()
|
||||
LFS.JWTSecretBase64, err = generate.NewJwtSecret()
|
||||
if err != nil {
|
||||
log.Fatal(4, "Error generating JWT Secret for custom config: %v", err)
|
||||
return
|
||||
@ -949,6 +961,41 @@ func NewContext() {
|
||||
}
|
||||
}
|
||||
|
||||
if err = Cfg.Section("oauth2").MapTo(&OAuth2); err != nil {
|
||||
log.Fatal(4, "Failed to OAuth2 settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if OAuth2.Enable {
|
||||
OAuth2.JWTSecretBytes = make([]byte, 32)
|
||||
n, err := base64.RawURLEncoding.Decode(OAuth2.JWTSecretBytes, []byte(OAuth2.JWTSecretBase64))
|
||||
|
||||
if err != nil || n != 32 {
|
||||
OAuth2.JWTSecretBase64, err = generate.NewJwtSecret()
|
||||
if err != nil {
|
||||
log.Fatal(4, "error generating JWT secret: %v", err)
|
||||
return
|
||||
}
|
||||
cfg := ini.Empty()
|
||||
if com.IsFile(CustomConf) {
|
||||
if err := cfg.Append(CustomConf); err != nil {
|
||||
log.Error(4, "failed to load custom conf %s: %v", CustomConf, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
cfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(CustomConf), os.ModePerm); err != nil {
|
||||
log.Fatal(4, "failed to create '%s': %v", CustomConf, err)
|
||||
return
|
||||
}
|
||||
if err := cfg.SaveTo(CustomConf); err != nil {
|
||||
log.Fatal(4, "error saving generating JWT secret to custom config: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sec = Cfg.Section("security")
|
||||
InstallLock = sec.Key("INSTALL_LOCK").MustBool(false)
|
||||
SecretKey = sec.Key("SECRET_KEY").MustString("!#@FDEWREWR&*(")
|
||||
|
@ -243,6 +243,13 @@ openid_register_desc = The chosen OpenID URI is unknown. Associate it with a new
|
||||
openid_signin_desc = Enter your OpenID URI. For example: https://anne.me, bob.openid.org.cn or gnusocial.net/carry.
|
||||
disable_forgot_password_mail = Password reset is disabled. Please contact your site administrator.
|
||||
email_domain_blacklisted = You cannot register with your email address.
|
||||
authorize_application = Authorize Application
|
||||
authroize_redirect_notice = You will be redirected to %s if you authorize this application.
|
||||
authorize_application_created_by = This application was created by %s.
|
||||
authorize_application_description = If you grant the access, it will be able to access and write to all your account information, including private repos and organisations.
|
||||
authorize_title = Authorize "%s" to access your account?
|
||||
authorization_failed = Authorization failed
|
||||
authorization_failed_desc = The authorization failed because we detected an invalid request. Please contact the maintainer of the app you've tried to authorize.
|
||||
|
||||
[mail]
|
||||
activate_account = Please activate your account
|
||||
@ -466,6 +473,31 @@ access_token_deletion = Delete Access Token
|
||||
access_token_deletion_desc = Deleting a token will revoke access to your account for applications using it. Continue?
|
||||
delete_token_success = The token has been deleted. Applications using it no longer have access to your account.
|
||||
|
||||
manage_oauth2_applications = Manage OAuth2 Applications
|
||||
edit_oauth2_application = Edit OAuth2 Application
|
||||
oauth2_applications_desc = OAuth2 applications enables your third-party application to securely authenticate users at this Gitea instance.
|
||||
remove_oauth2_application = Remove OAuth2 Application
|
||||
remove_oauth2_application_desc = Removing an OAuth2 application will revoke access to all signed access tokes. Continue?
|
||||
remove_oauth2_application_success = The application has been deleted.
|
||||
create_oauth2_application = Create a new OAuth2 Application
|
||||
create_oauth2_application_button = Create Application
|
||||
create_oauth2_application_success = You've successfully created a new OAuth2 application.
|
||||
update_oauth2_application_success = You've successfully updated the OAuth2 application.
|
||||
oauth2_application_name = Application Name
|
||||
oauth2_select_type = Which application type fits?
|
||||
oauth2_type_web = Web (e.g. Node.JS, Tomcat, Go)
|
||||
oauth2_type_native = Native (e.g. Mobile, Desktop, Browser)
|
||||
oauth2_redirect_uri = Redirect URI
|
||||
save_application = Save
|
||||
oauth2_client_id = Client ID
|
||||
oauth2_client_secret = Client Secret
|
||||
oauth2_regenerate_secret = Regenerate Secret
|
||||
oauth2_regenerate_secret_hint = Lost your secret?
|
||||
oauth2_client_secret_hint = The secret won't be visible if you revisit this page. Please save your secret.
|
||||
oauth2_application_edit = Edit
|
||||
oauth2_application_create_description = OAuth2 applications gives your third-party application access to user accounts on this instance.
|
||||
oauth2_application_remove_description = Removing an OAuth2 application will prevent it to access authorized user accounts on this instance. Continue?
|
||||
|
||||
twofa_desc = Two-factor authentication enhances the security of your account.
|
||||
twofa_is_enrolled = Your account is currently <strong>enrolled</strong> in two-factor authentication.
|
||||
twofa_not_enrolled = Your account is not currently enrolled in two-factor authentication.
|
||||
|
File diff suppressed because one or more lines are too long
@ -633,3 +633,7 @@ footer {
|
||||
.archived-icon{
|
||||
color: lighten(#000, 70%) !important;
|
||||
}
|
||||
|
||||
.oauth2-authorize-application-box {
|
||||
margin-top: 3em !important;
|
||||
}
|
||||
|
@ -266,7 +266,7 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) {
|
||||
cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
|
||||
cfg.Section("server").Key("LFS_CONTENT_PATH").SetValue(form.LFSRootPath)
|
||||
var secretKey string
|
||||
if secretKey, err = generate.NewLfsJwtSecret(); err != nil {
|
||||
if secretKey, err = generate.NewJwtSecret(); err != nil {
|
||||
ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form)
|
||||
return
|
||||
}
|
||||
|
@ -257,6 +257,14 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||
})
|
||||
}, reqSignOut)
|
||||
|
||||
m.Group("/login/oauth", func() {
|
||||
m.Get("/authorize", bindIgnErr(auth.AuthorizationForm{}), user.AuthorizeOAuth)
|
||||
m.Post("/grant", bindIgnErr(auth.GrantApplicationForm{}), user.GrantApplicationOAuth)
|
||||
// TODO manage redirection
|
||||
m.Post("/authorize", bindIgnErr(auth.AuthorizationForm{}), user.AuthorizeOAuth)
|
||||
}, ignSignInAndCsrf, reqSignIn)
|
||||
m.Post("/login/oauth/access_token", bindIgnErr(auth.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
|
||||
|
||||
m.Group("/user/settings", func() {
|
||||
m.Get("", userSetting.Profile)
|
||||
m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost)
|
||||
@ -291,6 +299,12 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||
}, openIDSignInEnabled)
|
||||
m.Post("/account_link", userSetting.DeleteAccountLink)
|
||||
})
|
||||
m.Group("/applications/oauth2", func() {
|
||||
m.Get("/:id", userSetting.OAuth2ApplicationShow)
|
||||
m.Post("/:id", bindIgnErr(auth.EditOAuth2ApplicationForm{}), userSetting.OAuthApplicationsEdit)
|
||||
m.Post("", bindIgnErr(auth.EditOAuth2ApplicationForm{}), userSetting.OAuthApplicationsPost)
|
||||
m.Post("/delete", userSetting.DeleteOAuth2Application)
|
||||
})
|
||||
m.Combo("/applications").Get(userSetting.Applications).
|
||||
Post(bindIgnErr(auth.NewAccessTokenForm{}), userSetting.ApplicationsPost)
|
||||
m.Post("/applications/delete", userSetting.DeleteApplication)
|
||||
|
452
routers/user/oauth.go
Normal file
452
routers/user/oauth.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -74,4 +74,12 @@ func loadApplicationsData(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
ctx.Data["Tokens"] = tokens
|
||||
ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable
|
||||
if setting.OAuth2.Enable {
|
||||
ctx.Data["Applications"], err = models.GetOAuth2ApplicationsByUserID(ctx.User.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
112
routers/user/setting/oauth2.go
Normal file
112
routers/user/setting/oauth2.go
Normal file
@ -0,0 +1,112 @@
|
||||
// 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 setting
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
tplSettingsOAuthApplications base.TplName = "user/settings/applications_oauth2_edit"
|
||||
)
|
||||
|
||||
// OAuthApplicationsPost response for adding a oauth2 application
|
||||
func OAuthApplicationsPost(ctx *context.Context, form auth.EditOAuth2ApplicationForm) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
|
||||
if ctx.HasError() {
|
||||
loadApplicationsData(ctx)
|
||||
|
||||
ctx.HTML(200, tplSettingsApplications)
|
||||
return
|
||||
}
|
||||
// TODO validate redirect URI
|
||||
app, err := models.CreateOAuth2Application(models.CreateOAuth2ApplicationOptions{
|
||||
Name: form.Name,
|
||||
RedirectURIs: []string{form.RedirectURI},
|
||||
UserID: ctx.User.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateOAuth2Application", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("settings.create_oauth2_application_success"))
|
||||
ctx.Data["App"] = app
|
||||
ctx.Data["ClientSecret"], err = app.GenerateClientSecret()
|
||||
if err != nil {
|
||||
ctx.ServerError("GenerateClientSecret", err)
|
||||
return
|
||||
}
|
||||
ctx.HTML(200, tplSettingsOAuthApplications)
|
||||
}
|
||||
|
||||
// OAuthApplicationsEdit response for editing oauth2 application
|
||||
func OAuthApplicationsEdit(ctx *context.Context, form auth.EditOAuth2ApplicationForm) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
|
||||
if ctx.HasError() {
|
||||
loadApplicationsData(ctx)
|
||||
|
||||
ctx.HTML(200, tplSettingsApplications)
|
||||
return
|
||||
}
|
||||
// TODO validate redirect URI
|
||||
if err := models.UpdateOAuth2Application(models.UpdateOAuth2ApplicationOptions{
|
||||
ID: ctx.ParamsInt64("id"),
|
||||
Name: form.Name,
|
||||
RedirectURIs: []string{form.RedirectURI},
|
||||
UserID: ctx.User.ID,
|
||||
}); err != nil {
|
||||
ctx.ServerError("UpdateOAuth2Application", err)
|
||||
return
|
||||
}
|
||||
var err error
|
||||
if ctx.Data["App"], err = models.GetOAuth2ApplicationByID(ctx.ParamsInt64("id")); err != nil {
|
||||
ctx.ServerError("GetOAuth2ApplicationByID", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
|
||||
ctx.HTML(200, tplSettingsOAuthApplications)
|
||||
}
|
||||
|
||||
// OAuth2ApplicationShow displays the given application
|
||||
func OAuth2ApplicationShow(ctx *context.Context) {
|
||||
app, err := models.GetOAuth2ApplicationByID(ctx.ParamsInt64("id"))
|
||||
if err != nil {
|
||||
if models.IsErrOAuthApplicationNotFound(err) {
|
||||
ctx.NotFound("Application not found", err)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetOAuth2ApplicationByID", err)
|
||||
return
|
||||
}
|
||||
if app.UID != ctx.User.ID {
|
||||
ctx.NotFound("Application not found", nil)
|
||||
return
|
||||
}
|
||||
ctx.Data["App"] = app
|
||||
ctx.HTML(200, tplSettingsOAuthApplications)
|
||||
}
|
||||
|
||||
// DeleteOAuth2Application deletes the given oauth2 application
|
||||
func DeleteOAuth2Application(ctx *context.Context) {
|
||||
if err := models.DeleteOAuth2Application(ctx.QueryInt64("id"), ctx.User.ID); err != nil {
|
||||
ctx.ServerError("DeleteOAuth2Application", err)
|
||||
return
|
||||
}
|
||||
log.Trace("OAuth2 Application deleted: %s", ctx.User.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.remove_oauth2_application_success"))
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"redirect": setting.AppSubURL + "/user/settings/applications",
|
||||
})
|
||||
}
|
31
templates/user/auth/grant.tmpl
Normal file
31
templates/user/auth/grant.tmpl
Normal file
@ -0,0 +1,31 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="ui one column stackable center aligned page grid oauth2-authorize-application-box">
|
||||
<div class="column seven wide">
|
||||
<div class="ui middle centered raised segments">
|
||||
<h3 class="ui top attached header">
|
||||
{{.i18n.Tr "auth.authorize_title" .Application.Name}}
|
||||
</h3>
|
||||
<div class="ui attached segment">
|
||||
{{template "base/alert" .}}
|
||||
<p>
|
||||
<b>{{.i18n.Tr "auth.authorize_application_description"}}</b><br/>
|
||||
{{.i18n.Tr "auth.authorize_application_created_by" .ApplicationUserLink | Str2html}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="ui attached segment">
|
||||
<p>{{.i18n.Tr "auth.authroize_redirect_notice" .ApplicationRedirectDomainHTML | Str2html}}</p>
|
||||
</div>
|
||||
<div class="ui attached segment">
|
||||
<form method="post" action="{{.AppSubUrl}}/login/oauth/grant">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="client_id" value="{{.Application.ClientID}}">
|
||||
<input type="hidden" name="state" value="{{.State}}">
|
||||
<input type="hidden" name="redirect_uri" value="{{.RedirectURI}}">
|
||||
<input type="submit" id="authorize-app" value="{{.i18n.Tr "auth.authorize_application"}}" class="ui red inline button"/>
|
||||
<a href="{{.RedirectURI}}" class="ui basic primary inline button">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
16
templates/user/auth/grant_error.tmpl
Normal file
16
templates/user/auth/grant_error.tmpl
Normal file
@ -0,0 +1,16 @@
|
||||
{{template "base/head" .}}
|
||||
{{if .IsRepo}}<div class="repository">{{template "repo/header" .}}</div>{{end}}
|
||||
<div class="ui one column stackable center aligned page grid oauth2-authorize-application-box">
|
||||
<div class="column seven wide">
|
||||
<div class="ui middle centered raised segments">
|
||||
<h1 class="ui top attached header">
|
||||
{{.i18n.Tr "auth.authorization_failed" }}
|
||||
</h1>
|
||||
<h3 class="ui attached segment">{{.Error.ErrorDescription}}</h3>
|
||||
<div class="ui attached segment">
|
||||
<p>{{.i18n.Tr "auth.authorization_failed_desc"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
@ -45,6 +45,10 @@
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{if .EnableOAuth2}}
|
||||
{{template "user/settings/applications_oauth2" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user