Skip to content

Commit e777c6b

Browse files
jonasfranztechknowlogick
authored andcommitted
Integrate OAuth2 Provider (#5378)
1 parent 9d3732d commit e777c6b

37 files changed

+2667
-11
lines changed

Gopkg.lock

+4-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/generate.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func runGenerateInternalToken(c *cli.Context) error {
6363
}
6464

6565
func runGenerateLfsJwtSecret(c *cli.Context) error {
66-
JWTSecretBase64, err := generate.NewLfsJwtSecret()
66+
JWTSecretBase64, err := generate.NewJwtSecret()
6767
if err != nil {
6868
return err
6969
}

custom/conf/app.ini.sample

+10
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,16 @@ DEFAULT_PAGING_NUM = 30
654654
; Default and maximum number of items per page for git trees api
655655
DEFAULT_GIT_TREES_PER_PAGE = 1000
656656

657+
[oauth2]
658+
; Enables OAuth2 provider
659+
ENABLED = true
660+
; Lifetime of an OAuth2 access token in seconds
661+
ACCESS_TOKEN_EXPIRATION_TIME=3600
662+
; Lifetime of an OAuth2 access token in hours
663+
REFRESH_TOKEN_EXPIRATION_TIME=730
664+
; OAuth2 authentication secret for access and refresh tokens, change this a unique string.
665+
JWT_SECRET=Bk0yK7Y9g_p56v86KaHqjSbxvNvu3SbKoOdOt2ZcXvU
666+
657667
[i18n]
658668
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
659669
NAMES = English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,Українська,日本語,español,português do Brasil,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+7
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,13 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
345345
- `DEFAULT_PAGING_NUM`: **30**: Default paging number of api.
346346
- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: Default and maximum number of items per page for git trees api.
347347

348+
## OAuth2 (`oauth2`)
349+
350+
- `ENABLED`: **true**: Enables OAuth2 provider.
351+
- `ACCESS_TOKEN_EXPIRATION_TIME`: **3600**: Lifetime of an OAuth2 access token in seconds
352+
- `REFRESH_TOKEN_EXPIRATION_TIME`: **730**: Lifetime of an OAuth2 access token in hours
353+
- `JWT_SECRET`: **\<empty\>**: OAuth2 authentication secret for access and refresh tokens, change this a unique string.
354+
348355
## i18n (`i18n`)
349356

350357
- `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

integrations/oauth_test.go

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package integrations
6+
7+
import (
8+
"encoding/json"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
const defaultAuthorize = "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate"
15+
16+
func TestNoClientID(t *testing.T) {
17+
prepareTestEnv(t)
18+
req := NewRequest(t, "GET", "/login/oauth/authorize")
19+
ctx := loginUser(t, "user2")
20+
ctx.MakeRequest(t, req, 400)
21+
}
22+
23+
func TestLoginRedirect(t *testing.T) {
24+
prepareTestEnv(t)
25+
req := NewRequest(t, "GET", "/login/oauth/authorize")
26+
assert.Contains(t, MakeRequest(t, req, 302).Body.String(), "/user/login")
27+
}
28+
29+
func TestShowAuthorize(t *testing.T) {
30+
prepareTestEnv(t)
31+
req := NewRequest(t, "GET", defaultAuthorize)
32+
ctx := loginUser(t, "user4")
33+
resp := ctx.MakeRequest(t, req, 200)
34+
35+
htmlDoc := NewHTMLParser(t, resp.Body)
36+
htmlDoc.AssertElement(t, "#authorize-app", true)
37+
htmlDoc.GetCSRF()
38+
}
39+
40+
func TestRedirectWithExistingGrant(t *testing.T) {
41+
prepareTestEnv(t)
42+
req := NewRequest(t, "GET", defaultAuthorize)
43+
ctx := loginUser(t, "user1")
44+
resp := ctx.MakeRequest(t, req, 302)
45+
u, err := resp.Result().Location()
46+
assert.NoError(t, err)
47+
assert.Equal(t, "thestate", u.Query().Get("state"))
48+
assert.Truef(t, len(u.Query().Get("code")) > 30, "authorization code '%s' should be longer then 30", u.Query().Get("code"))
49+
}
50+
51+
func TestAccessTokenExchange(t *testing.T) {
52+
prepareTestEnv(t)
53+
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
54+
"grant_type": "authorization_code",
55+
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
56+
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
57+
"redirect_uri": "a",
58+
"code": "authcode",
59+
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
60+
})
61+
resp := MakeRequest(t, req, 200)
62+
type response struct {
63+
AccessToken string `json:"access_token"`
64+
TokenType string `json:"token_type"`
65+
ExpiresIn int64 `json:"expires_in"`
66+
RefreshToken string `json:"refresh_token"`
67+
}
68+
parsed := new(response)
69+
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
70+
assert.True(t, len(parsed.AccessToken) > 10)
71+
assert.True(t, len(parsed.RefreshToken) > 10)
72+
}
73+
74+
func TestAccessTokenExchangeWithoutPKCE(t *testing.T) {
75+
prepareTestEnv(t)
76+
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
77+
"grant_type": "authorization_code",
78+
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
79+
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
80+
"redirect_uri": "a",
81+
"code": "authcode",
82+
})
83+
MakeRequest(t, req, 400)
84+
}
85+
86+
func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
87+
prepareTestEnv(t)
88+
// invalid client id
89+
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
90+
"grant_type": "authorization_code",
91+
"client_id": "???",
92+
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
93+
"redirect_uri": "a",
94+
"code": "authcode",
95+
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
96+
})
97+
MakeRequest(t, req, 400)
98+
// invalid client secret
99+
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
100+
"grant_type": "authorization_code",
101+
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
102+
"client_secret": "???",
103+
"redirect_uri": "a",
104+
"code": "authcode",
105+
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
106+
})
107+
MakeRequest(t, req, 400)
108+
// invalid redirect uri
109+
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
110+
"grant_type": "authorization_code",
111+
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
112+
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
113+
"redirect_uri": "???",
114+
"code": "authcode",
115+
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
116+
})
117+
MakeRequest(t, req, 400)
118+
// invalid authorization code
119+
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
120+
"grant_type": "authorization_code",
121+
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
122+
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
123+
"redirect_uri": "a",
124+
"code": "???",
125+
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
126+
})
127+
MakeRequest(t, req, 400)
128+
// invalid grant_type
129+
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
130+
"grant_type": "???",
131+
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
132+
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
133+
"redirect_uri": "a",
134+
"code": "authcode",
135+
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
136+
})
137+
MakeRequest(t, req, 400)
138+
}

models/error.go

+39
Original file line numberDiff line numberDiff line change
@@ -1398,3 +1398,42 @@ func IsErrReviewNotExist(err error) bool {
13981398
func (err ErrReviewNotExist) Error() string {
13991399
return fmt.Sprintf("review does not exist [id: %d]", err.ID)
14001400
}
1401+
1402+
// ________ _____ __ .__
1403+
// \_____ \ / _ \ __ ___/ |_| |__
1404+
// / | \ / /_\ \| | \ __\ | \
1405+
// / | \/ | \ | /| | | Y \
1406+
// \_______ /\____|__ /____/ |__| |___| /
1407+
// \/ \/ \/
1408+
1409+
// ErrOAuthClientIDInvalid will be thrown if client id cannot be found
1410+
type ErrOAuthClientIDInvalid struct {
1411+
ClientID string
1412+
}
1413+
1414+
// IsErrOauthClientIDInvalid checks if an error is a ErrReviewNotExist.
1415+
func IsErrOauthClientIDInvalid(err error) bool {
1416+
_, ok := err.(ErrOAuthClientIDInvalid)
1417+
return ok
1418+
}
1419+
1420+
// Error returns the error message
1421+
func (err ErrOAuthClientIDInvalid) Error() string {
1422+
return fmt.Sprintf("Client ID invalid [Client ID: %s]", err.ClientID)
1423+
}
1424+
1425+
// ErrOAuthApplicationNotFound will be thrown if id cannot be found
1426+
type ErrOAuthApplicationNotFound struct {
1427+
ID int64
1428+
}
1429+
1430+
// IsErrOAuthApplicationNotFound checks if an error is a ErrReviewNotExist.
1431+
func IsErrOAuthApplicationNotFound(err error) bool {
1432+
_, ok := err.(ErrOAuthApplicationNotFound)
1433+
return ok
1434+
}
1435+
1436+
// Error returns the error message
1437+
func (err ErrOAuthApplicationNotFound) Error() string {
1438+
return fmt.Sprintf("OAuth application not found [ID: %d]", err.ID)
1439+
}
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-
2+
id: 1
3+
uid: 1
4+
name: "Test"
5+
client_id: "da7da3ba-9a13-4167-856f-3899de0b0138"
6+
client_secret: "$2a$10$UYRgUSgekzBp6hYe8pAdc.cgB4Gn06QRKsORUnIYTYQADs.YR/uvi" # bcrypt of "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=
7+
redirect_uris: '["a"]'
8+
created_unix: 1546869730
9+
updated_unix: 1546869730
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
- id: 1
2+
grant_id: 1
3+
code: "authcode"
4+
code_challenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg" # Code Verifier: N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt
5+
code_challenge_method: "S256"
6+
redirect_uri: "a"
7+
valid_until: 3546869730
8+

models/fixtures/oauth2_grant.yml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
- id: 1
2+
user_id: 1
3+
application_id: 1
4+
counter: 1
5+
created_unix: 1546869730
6+
updated_unix: 1546869730

models/models.go

+3
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ func init() {
125125
new(U2FRegistration),
126126
new(TeamUnit),
127127
new(Review),
128+
new(OAuth2Application),
129+
new(OAuth2AuthorizationCode),
130+
new(OAuth2Grant),
128131
)
129132

130133
gonicNames := []string{"SSL", "UID"}

0 commit comments

Comments
 (0)