summaryrefslogtreecommitdiffstats
path: root/routers/user
diff options
context:
space:
mode:
authorJohnny Oskarsson <johnny@joskar.se>2021-01-01 17:33:27 +0100
committerGitHub <noreply@github.com>2021-01-02 00:33:27 +0800
commita07e67d9cc39506c149cfadcf0d6f2ab234cf7f6 (patch)
treee4724060cbd74a2374be9bab8c4f13773ee3b270 /routers/user
parent4f2f08bd804a6b900e9e7e7bd7f6e7f073581966 (diff)
downloadgitea-a07e67d9cc39506c149cfadcf0d6f2ab234cf7f6.tar.gz
gitea-a07e67d9cc39506c149cfadcf0d6f2ab234cf7f6.zip
Minimal OpenID Connect implementation (#14139)
This is "minimal" in the sense that only the Authorization Code Flow from OpenID Connect Core is implemented. No discovery, no configuration endpoint, and no user scope management. OpenID Connect is an extension to the (already implemented) OAuth 2.0 protocol, and essentially an `id_token` JWT is added to the access token endpoint response when using the Authorization Code Flow. I also added support for the "nonce" field since it is required to be used in the id_token if the client decides to include it in its initial request. In order to enable this extension an OAuth 2.0 scope containing "openid" is needed. Other OAuth 2.0 requests should not be impacted by this change. This minimal implementation is enough to enable single sign-on (SSO) for other sites, e.g. by using something like `mod_auth_openidc` to only allow access to a CI server if a user has logged into Gitea. Fixes: #1310 Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: zeripath <art27@cantab.net>
Diffstat (limited to 'routers/user')
-rw-r--r--routers/user/oauth.go53
1 files changed, 49 insertions, 4 deletions
diff --git a/routers/user/oauth.go b/routers/user/oauth.go
index 12665e94db..dda1268f8a 100644
--- a/routers/user/oauth.go
+++ b/routers/user/oauth.go
@@ -107,9 +107,10 @@ type AccessTokenResponse struct {
TokenType TokenType `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
+ IDToken string `json:"id_token,omitempty"`
}
-func newAccessTokenResponse(grant *models.OAuth2Grant) (*AccessTokenResponse, *AccessTokenError) {
+func newAccessTokenResponse(grant *models.OAuth2Grant, clientSecret string) (*AccessTokenResponse, *AccessTokenError) {
if setting.OAuth2.InvalidateRefreshTokens {
if err := grant.IncreaseCounter(); err != nil {
return nil, &AccessTokenError{
@@ -153,11 +154,40 @@ func newAccessTokenResponse(grant *models.OAuth2Grant) (*AccessTokenResponse, *A
}
}
+ // generate OpenID Connect id_token
+ signedIDToken := ""
+ if grant.ScopeContains("openid") {
+ app, err := models.GetOAuth2ApplicationByID(grant.ApplicationID)
+ if err != nil {
+ return nil, &AccessTokenError{
+ ErrorCode: AccessTokenErrorCodeInvalidRequest,
+ ErrorDescription: "cannot find application",
+ }
+ }
+ idToken := &models.OIDCToken{
+ StandardClaims: jwt.StandardClaims{
+ ExpiresAt: expirationDate.AsTime().Unix(),
+ Issuer: setting.AppURL,
+ Audience: app.ClientID,
+ Subject: fmt.Sprint(grant.UserID),
+ },
+ Nonce: grant.Nonce,
+ }
+ signedIDToken, err = idToken.SignToken(clientSecret)
+ if err != nil {
+ return nil, &AccessTokenError{
+ ErrorCode: AccessTokenErrorCodeInvalidRequest,
+ ErrorDescription: "cannot sign token",
+ }
+ }
+ }
+
return &AccessTokenResponse{
AccessToken: signedAccessToken,
TokenType: TokenTypeBearer,
ExpiresIn: setting.OAuth2.AccessTokenExpirationTime,
RefreshToken: signedRefreshToken,
+ IDToken: signedIDToken,
}, nil
}
@@ -264,6 +294,13 @@ func AuthorizeOAuth(ctx *context.Context, form auth.AuthorizationForm) {
handleServerError(ctx, form.State, form.RedirectURI)
return
}
+ // Update nonce to reflect the new session
+ if len(form.Nonce) > 0 {
+ err := grant.SetNonce(form.Nonce)
+ if err != nil {
+ log.Error("Unable to update nonce: %v", err)
+ }
+ }
ctx.Redirect(redirect.String(), 302)
return
}
@@ -272,6 +309,8 @@ func AuthorizeOAuth(ctx *context.Context, form auth.AuthorizationForm) {
ctx.Data["Application"] = app
ctx.Data["RedirectURI"] = form.RedirectURI
ctx.Data["State"] = form.State
+ ctx.Data["Scope"] = form.Scope
+ ctx.Data["Nonce"] = form.Nonce
ctx.Data["ApplicationUserLink"] = "<a href=\"" + html.EscapeString(setting.AppURL) + html.EscapeString(url.PathEscape(app.User.LowerName)) + "\">@" + html.EscapeString(app.User.Name) + "</a>"
ctx.Data["ApplicationRedirectDomainHTML"] = "<strong>" + html.EscapeString(form.RedirectURI) + "</strong>"
// TODO document SESSION <=> FORM
@@ -313,7 +352,7 @@ func GrantApplicationOAuth(ctx *context.Context, form auth.GrantApplicationForm)
ctx.ServerError("GetOAuth2ApplicationByClientID", err)
return
}
- grant, err := app.CreateGrant(ctx.User.ID)
+ grant, err := app.CreateGrant(ctx.User.ID, form.Scope)
if err != nil {
handleAuthorizeError(ctx, AuthorizeError{
State: form.State,
@@ -322,6 +361,12 @@ func GrantApplicationOAuth(ctx *context.Context, form auth.GrantApplicationForm)
}, form.RedirectURI)
return
}
+ if len(form.Nonce) > 0 {
+ err := grant.SetNonce(form.Nonce)
+ if err != nil {
+ log.Error("Unable to update nonce: %v", err)
+ }
+ }
var codeChallenge, codeChallengeMethod string
codeChallenge, _ = ctx.Session.Get("CodeChallenge").(string)
@@ -409,7 +454,7 @@ func handleRefreshToken(ctx *context.Context, form auth.AccessTokenForm) {
log.Warn("A client tried to use a refresh token for grant_id = %d was used twice!", grant.ID)
return
}
- accessToken, tokenErr := newAccessTokenResponse(grant)
+ accessToken, tokenErr := newAccessTokenResponse(grant, form.ClientSecret)
if tokenErr != nil {
handleAccessTokenError(ctx, *tokenErr)
return
@@ -471,7 +516,7 @@ func handleAuthorizationCode(ctx *context.Context, form auth.AccessTokenForm) {
ErrorDescription: "cannot proceed your request",
})
}
- resp, tokenErr := newAccessTokenResponse(authorizationCode.Grant)
+ resp, tokenErr := newAccessTokenResponse(authorizationCode.Grant, form.ClientSecret)
if tokenErr != nil {
handleAccessTokenError(ctx, *tokenErr)
return