2019-03-08 17:42:50 +01:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 19:20:29 +01:00
// SPDX-License-Identifier: MIT
2019-03-08 17:42:50 +01:00
2022-01-02 14:12:35 +01:00
package auth
2019-03-08 17:42:50 +01:00
import (
2022-05-20 16:08:52 +02:00
stdContext "context"
2019-03-11 03:54:59 +01:00
"encoding/base64"
2022-01-02 14:12:35 +01:00
"errors"
2019-03-08 17:42:50 +01:00
"fmt"
2020-08-28 06:37:05 +02:00
"html"
2022-01-02 14:12:35 +01:00
"io"
2021-04-05 17:30:52 +02:00
"net/http"
2019-03-08 17:42:50 +01:00
"net/url"
2019-03-11 03:54:59 +01:00
"strings"
2019-03-08 17:42:50 +01:00
2022-01-02 14:12:35 +01:00
"code.gitea.io/gitea/models/auth"
2022-08-25 04:31:57 +02:00
org_model "code.gitea.io/gitea/models/organization"
2021-11-24 10:49:20 +01:00
user_model "code.gitea.io/gitea/models/user"
2023-02-08 07:44:42 +01:00
auth_module "code.gitea.io/gitea/modules/auth"
2019-03-08 17:42:50 +01:00
"code.gitea.io/gitea/modules/base"
2023-02-08 07:44:42 +01:00
"code.gitea.io/gitea/modules/container"
2019-03-08 17:42:50 +01:00
"code.gitea.io/gitea/modules/context"
2021-07-24 18:03:58 +02:00
"code.gitea.io/gitea/modules/json"
2019-03-08 17:42:50 +01:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
2019-08-15 16:46:21 +02:00
"code.gitea.io/gitea/modules/timeutil"
2022-04-29 21:38:11 +02:00
"code.gitea.io/gitea/modules/util"
2021-01-26 16:36:53 +01:00
"code.gitea.io/gitea/modules/web"
2022-01-02 14:12:35 +01:00
"code.gitea.io/gitea/modules/web/middleware"
auth_service "code.gitea.io/gitea/services/auth"
2023-02-08 07:44:42 +01:00
source_service "code.gitea.io/gitea/services/auth/source"
2021-07-24 12:16:34 +02:00
"code.gitea.io/gitea/services/auth/source/oauth2"
2022-01-02 14:12:35 +01:00
"code.gitea.io/gitea/services/externalaccount"
2021-04-06 21:44:05 +02:00
"code.gitea.io/gitea/services/forms"
2022-01-02 14:12:35 +01:00
user_service "code.gitea.io/gitea/services/user"
2019-06-12 21:41:28 +02:00
2021-01-26 16:36:53 +01:00
"gitea.com/go-chi/binding"
2022-01-14 16:03:31 +01:00
"github.com/golang-jwt/jwt/v4"
2022-01-02 14:12:35 +01:00
"github.com/markbates/goth"
2022-06-20 17:37:54 +02:00
"github.com/markbates/goth/gothic"
2019-03-08 17:42:50 +01:00
)
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
2022-10-23 07:28:46 +02:00
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2.1
2019-03-08 17:42:50 +01:00
type AuthorizeErrorCode string
const (
// ErrorCodeInvalidRequest represents the according error in RFC 6749
ErrorCodeInvalidRequest AuthorizeErrorCode = "invalid_request"
// ErrorCodeUnauthorizedClient represents the according error in RFC 6749
ErrorCodeUnauthorizedClient AuthorizeErrorCode = "unauthorized_client"
// ErrorCodeAccessDenied represents the according error in RFC 6749
ErrorCodeAccessDenied AuthorizeErrorCode = "access_denied"
// ErrorCodeUnsupportedResponseType represents the according error in RFC 6749
ErrorCodeUnsupportedResponseType AuthorizeErrorCode = "unsupported_response_type"
// ErrorCodeInvalidScope represents the according error in RFC 6749
ErrorCodeInvalidScope AuthorizeErrorCode = "invalid_scope"
// ErrorCodeServerError represents the according error in RFC 6749
ErrorCodeServerError AuthorizeErrorCode = "server_error"
// ErrorCodeTemporaryUnavailable represents the according error in RFC 6749
ErrorCodeTemporaryUnavailable AuthorizeErrorCode = "temporarily_unavailable"
)
// AuthorizeError represents an error type specified in RFC 6749
2022-10-23 07:28:46 +02:00
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2.1
2019-03-08 17:42:50 +01:00
type AuthorizeError struct {
ErrorCode AuthorizeErrorCode ` json:"error" form:"error" `
ErrorDescription string
State string
}
// Error returns the error message
func ( err AuthorizeError ) Error ( ) string {
return fmt . Sprintf ( "%s: %s" , err . ErrorCode , err . ErrorDescription )
}
// AccessTokenErrorCode represents an error code specified in RFC 6749
2022-10-23 07:28:46 +02:00
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
2019-03-08 17:42:50 +01:00
type AccessTokenErrorCode string
const (
// AccessTokenErrorCodeInvalidRequest represents an error code specified in RFC 6749
AccessTokenErrorCodeInvalidRequest AccessTokenErrorCode = "invalid_request"
// AccessTokenErrorCodeInvalidClient represents an error code specified in RFC 6749
AccessTokenErrorCodeInvalidClient = "invalid_client"
// AccessTokenErrorCodeInvalidGrant represents an error code specified in RFC 6749
AccessTokenErrorCodeInvalidGrant = "invalid_grant"
// AccessTokenErrorCodeUnauthorizedClient represents an error code specified in RFC 6749
AccessTokenErrorCodeUnauthorizedClient = "unauthorized_client"
// AccessTokenErrorCodeUnsupportedGrantType represents an error code specified in RFC 6749
AccessTokenErrorCodeUnsupportedGrantType = "unsupported_grant_type"
// AccessTokenErrorCodeInvalidScope represents an error code specified in RFC 6749
AccessTokenErrorCodeInvalidScope = "invalid_scope"
)
// AccessTokenError represents an error response specified in RFC 6749
2022-10-23 07:28:46 +02:00
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
2019-03-08 17:42:50 +01:00
type AccessTokenError struct {
ErrorCode AccessTokenErrorCode ` json:"error" form:"error" `
ErrorDescription string ` json:"error_description" `
}
// Error returns the error message
func ( err AccessTokenError ) Error ( ) string {
return fmt . Sprintf ( "%s: %s" , err . ErrorCode , err . ErrorDescription )
}
2022-01-07 22:02:09 +01:00
// errCallback represents a oauth2 callback error
type errCallback struct {
Code string
Description string
}
func ( err errCallback ) Error ( ) string {
return err . Description
}
2019-03-08 17:42:50 +01:00
// TokenType specifies the kind of token
type TokenType string
const (
// TokenTypeBearer represents a token type specified in RFC 6749
TokenTypeBearer TokenType = "bearer"
// TokenTypeMAC represents a token type specified in RFC 6749
TokenTypeMAC = "mac"
)
// AccessTokenResponse represents a successful access token response
2022-10-23 07:28:46 +02:00
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2
2019-03-08 17:42:50 +01:00
type AccessTokenResponse struct {
2019-04-12 09:50:21 +02:00
AccessToken string ` json:"access_token" `
TokenType TokenType ` json:"token_type" `
ExpiresIn int64 ` json:"expires_in" `
RefreshToken string ` json:"refresh_token" `
2021-01-01 17:33:27 +01:00
IDToken string ` json:"id_token,omitempty" `
2019-03-08 17:42:50 +01:00
}
2022-05-20 16:08:52 +02:00
func newAccessTokenResponse ( ctx stdContext . Context , grant * auth . OAuth2Grant , serverKey , clientKey oauth2 . JWTSigningKey ) ( * AccessTokenResponse , * AccessTokenError ) {
2019-04-12 09:50:21 +02:00
if setting . OAuth2 . InvalidateRefreshTokens {
2022-05-20 16:08:52 +02:00
if err := grant . IncreaseCounter ( ctx ) ; err != nil {
2019-04-12 09:50:21 +02:00
return nil , & AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidGrant ,
ErrorDescription : "cannot increase the grant counter" ,
}
2019-03-08 17:42:50 +01:00
}
}
// generate access token to access the API
2019-08-15 16:46:21 +02:00
expirationDate := timeutil . TimeStampNow ( ) . Add ( setting . OAuth2 . AccessTokenExpirationTime )
2021-07-24 12:16:34 +02:00
accessToken := & oauth2 . Token {
2019-03-08 17:42:50 +01:00
GrantID : grant . ID ,
2021-07-24 12:16:34 +02:00
Type : oauth2 . TypeAccessToken ,
2022-01-20 22:52:56 +01:00
RegisteredClaims : jwt . RegisteredClaims {
ExpiresAt : jwt . NewNumericDate ( expirationDate . AsTime ( ) ) ,
2019-03-08 17:42:50 +01:00
} ,
}
2021-08-27 21:28:00 +02:00
signedAccessToken , err := accessToken . SignToken ( serverKey )
2019-03-08 17:42:50 +01:00
if err != nil {
return nil , & AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidRequest ,
ErrorDescription : "cannot sign token" ,
}
}
// generate refresh token to request an access token after it expired later
2022-01-20 22:52:56 +01:00
refreshExpirationDate := timeutil . TimeStampNow ( ) . Add ( setting . OAuth2 . RefreshTokenExpirationTime * 60 * 60 ) . AsTime ( )
2021-07-24 12:16:34 +02:00
refreshToken := & oauth2 . Token {
2019-03-08 17:42:50 +01:00
GrantID : grant . ID ,
Counter : grant . Counter ,
2021-07-24 12:16:34 +02:00
Type : oauth2 . TypeRefreshToken ,
2022-06-20 12:02:49 +02:00
RegisteredClaims : jwt . RegisteredClaims {
2022-01-20 22:52:56 +01:00
ExpiresAt : jwt . NewNumericDate ( refreshExpirationDate ) ,
2019-03-08 17:42:50 +01:00
} ,
}
2021-08-27 21:28:00 +02:00
signedRefreshToken , err := refreshToken . SignToken ( serverKey )
2019-03-08 17:42:50 +01:00
if err != nil {
return nil , & AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidRequest ,
ErrorDescription : "cannot sign token" ,
}
}
2021-01-01 17:33:27 +01:00
// generate OpenID Connect id_token
signedIDToken := ""
if grant . ScopeContains ( "openid" ) {
2022-05-20 16:08:52 +02:00
app , err := auth . GetOAuth2ApplicationByID ( ctx , grant . ApplicationID )
2021-01-01 17:33:27 +01:00
if err != nil {
return nil , & AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidRequest ,
ErrorDescription : "cannot find application" ,
}
}
2022-12-03 03:48:26 +01:00
user , err := user_model . GetUserByID ( ctx , grant . UserID )
2021-06-14 12:33:16 +02:00
if err != nil {
2021-11-24 10:49:20 +01:00
if user_model . IsErrUserNotExist ( err ) {
2021-06-14 12:33:16 +02:00
return nil , & AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidRequest ,
ErrorDescription : "cannot find user" ,
}
}
log . Error ( "Error loading user: %v" , err )
return nil , & AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidRequest ,
ErrorDescription : "server error" ,
}
}
2021-07-24 12:16:34 +02:00
idToken := & oauth2 . OIDCToken {
2022-01-20 22:52:56 +01:00
RegisteredClaims : jwt . RegisteredClaims {
ExpiresAt : jwt . NewNumericDate ( expirationDate . AsTime ( ) ) ,
2021-01-01 17:33:27 +01:00
Issuer : setting . AppURL ,
2022-01-20 22:52:56 +01:00
Audience : [ ] string { app . ClientID } ,
2021-01-01 17:33:27 +01:00
Subject : fmt . Sprint ( grant . UserID ) ,
} ,
Nonce : grant . Nonce ,
}
2021-06-14 12:33:16 +02:00
if grant . ScopeContains ( "profile" ) {
2022-06-17 00:29:54 +02:00
idToken . Name = user . GetDisplayName ( )
2021-08-19 18:11:30 +02:00
idToken . PreferredUsername = user . Name
idToken . Profile = user . HTMLURL ( )
idToken . Picture = user . AvatarLink ( )
idToken . Website = user . Website
idToken . Locale = user . Language
idToken . UpdatedAt = user . UpdatedUnix
2021-06-14 12:33:16 +02:00
}
if grant . ScopeContains ( "email" ) {
2021-08-19 18:11:30 +02:00
idToken . Email = user . Email
idToken . EmailVerified = user . IsActive
2021-06-14 12:33:16 +02:00
}
2021-10-22 11:19:24 +02:00
if grant . ScopeContains ( "groups" ) {
groups , err := getOAuthGroupsForUser ( user )
if err != nil {
log . Error ( "Error getting groups: %v" , err )
return nil , & AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidRequest ,
ErrorDescription : "server error" ,
}
}
idToken . Groups = groups
}
2021-06-14 12:33:16 +02:00
2021-08-27 21:28:00 +02:00
signedIDToken , err = idToken . SignToken ( clientKey )
2021-01-01 17:33:27 +01:00
if err != nil {
return nil , & AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidRequest ,
ErrorDescription : "cannot sign token" ,
}
}
}
2019-03-08 17:42:50 +01:00
return & AccessTokenResponse {
AccessToken : signedAccessToken ,
TokenType : TokenTypeBearer ,
ExpiresIn : setting . OAuth2 . AccessTokenExpirationTime ,
RefreshToken : signedRefreshToken ,
2021-01-01 17:33:27 +01:00
IDToken : signedIDToken ,
2019-03-08 17:42:50 +01:00
} , nil
}
2021-05-06 07:30:15 +02:00
type userInfoResponse struct {
2021-10-22 11:19:24 +02:00
Sub string ` json:"sub" `
Name string ` json:"name" `
Username string ` json:"preferred_username" `
Email string ` json:"email" `
Picture string ` json:"picture" `
Groups [ ] string ` json:"groups" `
2021-05-06 07:30:15 +02:00
}
// InfoOAuth manages request for userinfo endpoint
func InfoOAuth ( ctx * context . Context ) {
2022-03-22 08:03:22 +01:00
if ctx . Doer == nil || ctx . Data [ "AuthedMethod" ] != ( & auth_service . OAuth2 { } ) . Name ( ) {
2021-08-21 04:16:45 +02:00
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , ` Bearer realm="" ` )
2021-12-15 07:59:57 +01:00
ctx . PlainText ( http . StatusUnauthorized , "no valid authorization" )
2021-05-06 07:30:15 +02:00
return
}
2021-10-22 11:19:24 +02:00
2021-05-06 07:30:15 +02:00
response := & userInfoResponse {
2022-03-22 08:03:22 +01:00
Sub : fmt . Sprint ( ctx . Doer . ID ) ,
Name : ctx . Doer . FullName ,
Username : ctx . Doer . Name ,
Email : ctx . Doer . Email ,
Picture : ctx . Doer . AvatarLink ( ) ,
2021-05-06 07:30:15 +02:00
}
2021-10-22 11:19:24 +02:00
2022-03-22 08:03:22 +01:00
groups , err := getOAuthGroupsForUser ( ctx . Doer )
2021-10-22 11:19:24 +02:00
if err != nil {
ctx . ServerError ( "Oauth groups for user" , err )
return
}
response . Groups = groups
2021-05-06 07:30:15 +02:00
ctx . JSON ( http . StatusOK , response )
}
2021-10-22 11:19:24 +02:00
// returns a list of "org" and "org:team" strings,
// that the given user is a part of.
2021-11-24 10:49:20 +01:00
func getOAuthGroupsForUser ( user * user_model . User ) ( [ ] string , error ) {
2022-08-25 04:31:57 +02:00
orgs , err := org_model . GetUserOrgsList ( user )
2021-10-22 11:19:24 +02:00
if err != nil {
2022-10-24 21:29:17 +02:00
return nil , fmt . Errorf ( "GetUserOrgList: %w" , err )
2021-10-22 11:19:24 +02:00
}
var groups [ ] string
for _ , org := range orgs {
groups = append ( groups , org . Name )
2021-11-19 12:41:40 +01:00
teams , err := org . LoadTeams ( )
if err != nil {
2022-10-24 21:29:17 +02:00
return nil , fmt . Errorf ( "LoadTeams: %w" , err )
2021-10-22 11:19:24 +02:00
}
2021-11-19 12:41:40 +01:00
for _ , team := range teams {
2021-10-22 11:19:24 +02:00
if team . IsMember ( user . ID ) {
groups = append ( groups , org . Name + ":" + team . LowerName )
}
}
}
return groups , nil
}
2021-08-21 04:16:45 +02:00
// IntrospectOAuth introspects an oauth token
func IntrospectOAuth ( ctx * context . Context ) {
2022-03-22 08:03:22 +01:00
if ctx . Doer == nil {
2021-08-21 04:16:45 +02:00
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , ` Bearer realm="" ` )
2021-12-15 07:59:57 +01:00
ctx . PlainText ( http . StatusUnauthorized , "no valid authorization" )
2021-08-21 04:16:45 +02:00
return
}
var response struct {
Active bool ` json:"active" `
Scope string ` json:"scope,omitempty" `
2022-01-20 22:52:56 +01:00
jwt . RegisteredClaims
2021-08-21 04:16:45 +02:00
}
form := web . GetForm ( ctx ) . ( * forms . IntrospectTokenForm )
2021-08-27 21:28:00 +02:00
token , err := oauth2 . ParseToken ( form . Token , oauth2 . DefaultSigningKey )
2021-08-21 04:16:45 +02:00
if err == nil {
if token . Valid ( ) == nil {
2022-05-20 16:08:52 +02:00
grant , err := auth . GetOAuth2GrantByID ( ctx , token . GrantID )
2021-08-21 04:16:45 +02:00
if err == nil && grant != nil {
2022-05-20 16:08:52 +02:00
app , err := auth . GetOAuth2ApplicationByID ( ctx , grant . ApplicationID )
2021-08-21 04:16:45 +02:00
if err == nil && app != nil {
response . Active = true
response . Scope = grant . Scope
response . Issuer = setting . AppURL
2022-01-20 22:52:56 +01:00
response . Audience = [ ] string { app . ClientID }
2021-08-21 04:16:45 +02:00
response . Subject = fmt . Sprint ( grant . UserID )
}
}
}
}
ctx . JSON ( http . StatusOK , response )
}
2019-03-08 17:42:50 +01:00
// AuthorizeOAuth manages authorize requests
2021-01-26 16:36:53 +01:00
func AuthorizeOAuth ( ctx * context . Context ) {
2021-04-06 21:44:05 +02:00
form := web . GetForm ( ctx ) . ( * forms . AuthorizationForm )
2019-03-08 17:42:50 +01:00
errs := binding . Errors { }
2021-01-26 16:36:53 +01:00
errs = form . Validate ( ctx . Req , errs )
2019-06-12 21:41:28 +02:00
if len ( errs ) > 0 {
errstring := ""
for _ , e := range errs {
errstring += e . Error ( ) + "\n"
}
2019-06-13 06:23:45 +02:00
ctx . ServerError ( "AuthorizeOAuth: Validate: " , fmt . Errorf ( "errors occurred during validation: %s" , errstring ) )
2019-06-12 21:41:28 +02:00
return
}
2019-03-08 17:42:50 +01:00
2022-05-20 16:08:52 +02:00
app , err := auth . GetOAuth2ApplicationByClientID ( ctx , form . ClientID )
2019-03-08 17:42:50 +01:00
if err != nil {
2022-01-02 14:12:35 +01:00
if auth . IsErrOauthClientIDInvalid ( err ) {
2019-03-08 17:42:50 +01:00
handleAuthorizeError ( ctx , AuthorizeError {
ErrorCode : ErrorCodeUnauthorizedClient ,
ErrorDescription : "Client ID not registered" ,
State : form . State ,
} , "" )
return
}
ctx . ServerError ( "GetOAuth2ApplicationByClientID" , err )
return
}
2021-09-24 13:32:56 +02:00
2022-10-12 16:08:29 +02:00
var user * user_model . User
if app . UID != 0 {
2022-12-03 03:48:26 +01:00
user , err = user_model . GetUserByID ( ctx , app . UID )
2022-10-12 16:08:29 +02:00
if err != nil {
ctx . ServerError ( "GetUserByID" , err )
return
}
2019-03-08 17:42:50 +01:00
}
if ! app . ContainsRedirectURI ( form . RedirectURI ) {
handleAuthorizeError ( ctx , AuthorizeError {
ErrorCode : ErrorCodeInvalidRequest ,
ErrorDescription : "Unregistered Redirect URI" ,
State : form . State ,
} , "" )
return
}
if form . ResponseType != "code" {
handleAuthorizeError ( ctx , AuthorizeError {
ErrorCode : ErrorCodeUnsupportedResponseType ,
ErrorDescription : "Only code response type is supported." ,
State : form . State ,
} , form . RedirectURI )
return
}
// pkce support
switch form . CodeChallengeMethod {
case "S256" :
case "plain" :
if err := ctx . Session . Set ( "CodeChallengeMethod" , form . CodeChallengeMethod ) ; err != nil {
handleAuthorizeError ( ctx , AuthorizeError {
ErrorCode : ErrorCodeServerError ,
ErrorDescription : "cannot set code challenge method" ,
State : form . State ,
} , form . RedirectURI )
return
}
if err := ctx . Session . Set ( "CodeChallengeMethod" , form . CodeChallenge ) ; err != nil {
handleAuthorizeError ( ctx , AuthorizeError {
ErrorCode : ErrorCodeServerError ,
ErrorDescription : "cannot set code challenge" ,
State : form . State ,
} , form . RedirectURI )
return
}
2020-05-17 14:43:29 +02:00
// Here we're just going to try to release the session early
if err := ctx . Session . Release ( ) ; err != nil {
// we'll tolerate errors here as they *should* get saved elsewhere
log . Error ( "Unable to save changes to the session: %v" , err )
}
2019-03-08 17:42:50 +01:00
case "" :
2022-10-24 09:59:24 +02:00
// "Authorization servers SHOULD reject authorization requests from native apps that don't use PKCE by returning an error message"
// https://datatracker.ietf.org/doc/html/rfc8252#section-8.1
if ! app . ConfidentialClient {
// "the authorization endpoint MUST return the authorization error response with the "error" value set to "invalid_request""
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1
handleAuthorizeError ( ctx , AuthorizeError {
ErrorCode : ErrorCodeInvalidRequest ,
ErrorDescription : "PKCE is required for public clients" ,
State : form . State ,
} , form . RedirectURI )
return
}
2019-03-08 17:42:50 +01:00
default :
2022-10-24 09:59:24 +02:00
// "If the server supporting PKCE does not support the requested transformation, the authorization endpoint MUST return the authorization error response with "error" value set to "invalid_request"."
// https://www.rfc-editor.org/rfc/rfc7636#section-4.4.1
2019-03-08 17:42:50 +01:00
handleAuthorizeError ( ctx , AuthorizeError {
ErrorCode : ErrorCodeInvalidRequest ,
ErrorDescription : "unsupported code challenge method" ,
State : form . State ,
} , form . RedirectURI )
return
}
2022-05-20 16:08:52 +02:00
grant , err := app . GetGrantByUserID ( ctx , ctx . Doer . ID )
2019-03-08 17:42:50 +01:00
if err != nil {
handleServerError ( ctx , form . State , form . RedirectURI )
return
}
// Redirect if user already granted access
if grant != nil {
2022-05-20 16:08:52 +02:00
code , err := grant . GenerateNewAuthorizationCode ( ctx , form . RedirectURI , form . CodeChallenge , form . CodeChallengeMethod )
2019-03-08 17:42:50 +01:00
if err != nil {
handleServerError ( ctx , form . State , form . RedirectURI )
return
}
redirect , err := code . GenerateRedirectURI ( form . State )
if err != nil {
handleServerError ( ctx , form . State , form . RedirectURI )
return
}
2021-01-01 17:33:27 +01:00
// Update nonce to reflect the new session
if len ( form . Nonce ) > 0 {
2022-05-20 16:08:52 +02:00
err := grant . SetNonce ( ctx , form . Nonce )
2021-01-01 17:33:27 +01:00
if err != nil {
log . Error ( "Unable to update nonce: %v" , err )
}
}
2022-03-23 05:54:07 +01:00
ctx . Redirect ( redirect . String ( ) )
2019-03-08 17:42:50 +01:00
return
}
// show authorize page to grant access
ctx . Data [ "Application" ] = app
ctx . Data [ "RedirectURI" ] = form . RedirectURI
ctx . Data [ "State" ] = form . State
2021-01-01 17:33:27 +01:00
ctx . Data [ "Scope" ] = form . Scope
ctx . Data [ "Nonce" ] = form . Nonce
2022-10-12 16:08:29 +02:00
if user != nil {
ctx . Data [ "ApplicationCreatorLinkHTML" ] = fmt . Sprintf ( ` <a href="%s">@%s</a> ` , html . EscapeString ( user . HomeLink ( ) ) , html . EscapeString ( user . Name ) )
} else {
ctx . Data [ "ApplicationCreatorLinkHTML" ] = fmt . Sprintf ( ` <a href="%s">%s</a> ` , html . EscapeString ( setting . AppSubURL + "/" ) , html . EscapeString ( setting . AppName ) )
}
2020-08-28 06:37:05 +02:00
ctx . Data [ "ApplicationRedirectDomainHTML" ] = "<strong>" + html . EscapeString ( form . RedirectURI ) + "</strong>"
2019-03-08 17:42:50 +01:00
// TODO document SESSION <=> FORM
2019-06-12 21:41:28 +02:00
err = ctx . Session . Set ( "client_id" , app . ClientID )
if err != nil {
handleServerError ( ctx , form . State , form . RedirectURI )
log . Error ( err . Error ( ) )
return
}
err = ctx . Session . Set ( "redirect_uri" , form . RedirectURI )
if err != nil {
handleServerError ( ctx , form . State , form . RedirectURI )
log . Error ( err . Error ( ) )
return
}
err = ctx . Session . Set ( "state" , form . State )
if err != nil {
handleServerError ( ctx , form . State , form . RedirectURI )
log . Error ( err . Error ( ) )
return
}
2020-05-17 14:43:29 +02:00
// Here we're just going to try to release the session early
if err := ctx . Session . Release ( ) ; err != nil {
// we'll tolerate errors here as they *should* get saved elsewhere
log . Error ( "Unable to save changes to the session: %v" , err )
}
2021-04-05 17:30:52 +02:00
ctx . HTML ( http . StatusOK , tplGrantAccess )
2019-03-08 17:42:50 +01:00
}
// GrantApplicationOAuth manages the post request submitted when a user grants access to an application
2021-01-26 16:36:53 +01:00
func GrantApplicationOAuth ( ctx * context . Context ) {
2021-04-06 21:44:05 +02:00
form := web . GetForm ( ctx ) . ( * forms . GrantApplicationForm )
2019-03-08 17:42:50 +01:00
if ctx . Session . Get ( "client_id" ) != form . ClientID || ctx . Session . Get ( "state" ) != form . State ||
ctx . Session . Get ( "redirect_uri" ) != form . RedirectURI {
2021-04-05 17:30:52 +02:00
ctx . Error ( http . StatusBadRequest )
2019-03-08 17:42:50 +01:00
return
}
2022-05-20 16:08:52 +02:00
app , err := auth . GetOAuth2ApplicationByClientID ( ctx , form . ClientID )
2019-03-08 17:42:50 +01:00
if err != nil {
ctx . ServerError ( "GetOAuth2ApplicationByClientID" , err )
return
}
2022-05-20 16:08:52 +02:00
grant , err := app . CreateGrant ( ctx , ctx . Doer . ID , form . Scope )
2019-03-08 17:42:50 +01:00
if err != nil {
handleAuthorizeError ( ctx , AuthorizeError {
State : form . State ,
ErrorDescription : "cannot create grant for user" ,
ErrorCode : ErrorCodeServerError ,
} , form . RedirectURI )
return
}
2021-01-01 17:33:27 +01:00
if len ( form . Nonce ) > 0 {
2022-05-20 16:08:52 +02:00
err := grant . SetNonce ( ctx , form . Nonce )
2021-01-01 17:33:27 +01:00
if err != nil {
log . Error ( "Unable to update nonce: %v" , err )
}
}
2019-03-08 17:42:50 +01:00
var codeChallenge , codeChallengeMethod string
codeChallenge , _ = ctx . Session . Get ( "CodeChallenge" ) . ( string )
codeChallengeMethod , _ = ctx . Session . Get ( "CodeChallengeMethod" ) . ( string )
2022-05-20 16:08:52 +02:00
code , err := grant . GenerateNewAuthorizationCode ( ctx , form . RedirectURI , codeChallenge , codeChallengeMethod )
2019-03-08 17:42:50 +01:00
if err != nil {
handleServerError ( ctx , form . State , form . RedirectURI )
return
}
redirect , err := code . GenerateRedirectURI ( form . State )
if err != nil {
handleServerError ( ctx , form . State , form . RedirectURI )
2019-04-25 13:30:38 +02:00
return
2019-03-08 17:42:50 +01:00
}
2022-03-23 05:54:07 +01:00
ctx . Redirect ( redirect . String ( ) , http . StatusSeeOther )
2019-03-08 17:42:50 +01:00
}
2021-04-16 04:32:00 +02:00
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
func OIDCWellKnown ( ctx * context . Context ) {
t := ctx . Render . TemplateLookup ( "user/auth/oidc_wellknown" )
ctx . Resp . Header ( ) . Set ( "Content-Type" , "application/json" )
2021-06-17 23:56:46 +02:00
ctx . Data [ "SigningKey" ] = oauth2 . DefaultSigningKey
2021-04-16 04:32:00 +02:00
if err := t . Execute ( ctx . Resp , ctx . Data ) ; err != nil {
log . Error ( "%v" , err )
ctx . Error ( http . StatusInternalServerError )
}
}
2021-06-17 23:56:46 +02:00
// OIDCKeys generates the JSON Web Key Set
func OIDCKeys ( ctx * context . Context ) {
jwk , err := oauth2 . DefaultSigningKey . ToJWK ( )
if err != nil {
log . Error ( "Error converting signing key to JWK: %v" , err )
ctx . Error ( http . StatusInternalServerError )
return
}
jwk [ "use" ] = "sig"
jwks := map [ string ] [ ] map [ string ] string {
"keys" : {
jwk ,
} ,
}
ctx . Resp . Header ( ) . Set ( "Content-Type" , "application/json" )
2021-07-24 18:03:58 +02:00
enc := json . NewEncoder ( ctx . Resp )
2021-06-17 23:56:46 +02:00
if err := enc . Encode ( jwks ) ; err != nil {
log . Error ( "Failed to encode representation as json. Error: %v" , err )
}
}
2019-03-08 17:42:50 +01:00
// AccessTokenOAuth manages all access token requests by the client
2021-01-26 16:36:53 +01:00
func AccessTokenOAuth ( ctx * context . Context ) {
2021-04-06 21:44:05 +02:00
form := * web . GetForm ( ctx ) . ( * forms . AccessTokenForm )
2022-10-07 04:53:49 +02:00
// if there is no ClientID or ClientSecret in the request body, fill these fields by the Authorization header and ensure the provided field matches the Authorization header
if form . ClientID == "" || form . ClientSecret == "" {
2019-03-11 03:54:59 +01:00
authHeader := ctx . Req . Header . Get ( "Authorization" )
authContent := strings . SplitN ( authHeader , " " , 2 )
if len ( authContent ) == 2 && authContent [ 0 ] == "Basic" {
payload , err := base64 . StdEncoding . DecodeString ( authContent [ 1 ] )
if err != nil {
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidRequest ,
ErrorDescription : "cannot parse basic auth header" ,
} )
return
}
pair := strings . SplitN ( string ( payload ) , ":" , 2 )
if len ( pair ) != 2 {
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidRequest ,
ErrorDescription : "cannot parse basic auth header" ,
} )
return
}
2022-10-07 04:53:49 +02:00
if form . ClientID != "" && form . ClientID != pair [ 0 ] {
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidRequest ,
ErrorDescription : "client_id in request body inconsistent with Authorization header" ,
} )
return
}
2019-03-11 03:54:59 +01:00
form . ClientID = pair [ 0 ]
2022-10-07 04:53:49 +02:00
if form . ClientSecret != "" && form . ClientSecret != pair [ 1 ] {
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidRequest ,
ErrorDescription : "client_secret in request body inconsistent with Authorization header" ,
} )
return
}
2019-03-11 03:54:59 +01:00
form . ClientSecret = pair [ 1 ]
}
}
2021-06-17 23:56:46 +02:00
2021-08-27 21:28:00 +02:00
serverKey := oauth2 . DefaultSigningKey
clientKey := serverKey
if serverKey . IsSymmetric ( ) {
var err error
clientKey , err = oauth2 . CreateJWTSigningKey ( serverKey . SigningMethod ( ) . Alg ( ) , [ ] byte ( form . ClientSecret ) )
2021-06-17 23:56:46 +02:00
if err != nil {
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidRequest ,
ErrorDescription : "Error creating signing key" ,
} )
return
}
}
2019-03-08 17:42:50 +01:00
switch form . GrantType {
case "refresh_token" :
2021-08-27 21:28:00 +02:00
handleRefreshToken ( ctx , form , serverKey , clientKey )
2019-03-08 17:42:50 +01:00
case "authorization_code" :
2021-08-27 21:28:00 +02:00
handleAuthorizationCode ( ctx , form , serverKey , clientKey )
2019-03-08 17:42:50 +01:00
default :
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeUnsupportedGrantType ,
ErrorDescription : "Only refresh_token or authorization_code grant type is supported" ,
} )
}
}
2021-08-27 21:28:00 +02:00
func handleRefreshToken ( ctx * context . Context , form forms . AccessTokenForm , serverKey , clientKey oauth2 . JWTSigningKey ) {
2022-10-23 07:28:46 +02:00
app , err := auth . GetOAuth2ApplicationByClientID ( ctx , form . ClientID )
if err != nil {
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidClient ,
ErrorDescription : fmt . Sprintf ( "cannot load client with client id: %q" , form . ClientID ) ,
} )
return
}
// "The authorization server MUST ... require client authentication for confidential clients"
// https://datatracker.ietf.org/doc/html/rfc6749#section-6
if ! app . ValidateClientSecret ( [ ] byte ( form . ClientSecret ) ) {
errorDescription := "invalid client secret"
if form . ClientSecret == "" {
errorDescription = "invalid empty client secret"
}
// "invalid_client ... Client authentication failed"
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidClient ,
ErrorDescription : errorDescription ,
} )
return
}
2021-08-27 21:28:00 +02:00
token , err := oauth2 . ParseToken ( form . RefreshToken , serverKey )
2019-03-08 17:42:50 +01:00
if err != nil {
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeUnauthorizedClient ,
2022-09-28 21:10:27 +02:00
ErrorDescription : "unable to parse refresh token" ,
2019-03-08 17:42:50 +01:00
} )
return
}
// get grant before increasing counter
2022-05-20 16:08:52 +02:00
grant , err := auth . GetOAuth2GrantByID ( ctx , token . GrantID )
2019-03-08 17:42:50 +01:00
if err != nil || grant == nil {
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidGrant ,
ErrorDescription : "grant does not exist" ,
} )
return
}
// check if token got already used
2019-04-12 09:50:21 +02:00
if setting . OAuth2 . InvalidateRefreshTokens && ( grant . Counter != token . Counter || token . Counter == 0 ) {
2019-03-08 17:42:50 +01:00
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeUnauthorizedClient ,
ErrorDescription : "token was already used" ,
} )
log . Warn ( "A client tried to use a refresh token for grant_id = %d was used twice!" , grant . ID )
return
}
2022-05-20 16:08:52 +02:00
accessToken , tokenErr := newAccessTokenResponse ( ctx , grant , serverKey , clientKey )
2019-03-08 17:42:50 +01:00
if tokenErr != nil {
handleAccessTokenError ( ctx , * tokenErr )
return
}
2021-04-05 17:30:52 +02:00
ctx . JSON ( http . StatusOK , accessToken )
2019-03-08 17:42:50 +01:00
}
2021-08-27 21:28:00 +02:00
func handleAuthorizationCode ( ctx * context . Context , form forms . AccessTokenForm , serverKey , clientKey oauth2 . JWTSigningKey ) {
2022-05-20 16:08:52 +02:00
app , err := auth . GetOAuth2ApplicationByClientID ( ctx , form . ClientID )
2019-03-08 17:42:50 +01:00
if err != nil {
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidClient ,
2019-03-11 03:54:59 +01:00
ErrorDescription : fmt . Sprintf ( "cannot load client with client id: '%s'" , form . ClientID ) ,
2019-03-08 17:42:50 +01:00
} )
return
}
if ! app . ValidateClientSecret ( [ ] byte ( form . ClientSecret ) ) {
2022-10-07 04:53:49 +02:00
errorDescription := "invalid client secret"
if form . ClientSecret == "" {
errorDescription = "invalid empty client secret"
}
2019-03-08 17:42:50 +01:00
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeUnauthorizedClient ,
2022-10-07 04:53:49 +02:00
ErrorDescription : errorDescription ,
2019-03-08 17:42:50 +01:00
} )
return
}
if form . RedirectURI != "" && ! app . ContainsRedirectURI ( form . RedirectURI ) {
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeUnauthorizedClient ,
2022-09-28 21:10:27 +02:00
ErrorDescription : "unexpected redirect URI" ,
2019-03-08 17:42:50 +01:00
} )
return
}
2022-05-20 16:08:52 +02:00
authorizationCode , err := auth . GetOAuth2AuthorizationByCode ( ctx , form . Code )
2019-03-08 17:42:50 +01:00
if err != nil || authorizationCode == nil {
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeUnauthorizedClient ,
ErrorDescription : "client is not authorized" ,
} )
return
}
// check if code verifier authorizes the client, PKCE support
if ! authorizationCode . ValidateCodeChallenge ( form . CodeVerifier ) {
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeUnauthorizedClient ,
2022-09-28 21:10:27 +02:00
ErrorDescription : "failed PKCE code challenge" ,
2019-03-08 17:42:50 +01:00
} )
return
}
// check if granted for this application
if authorizationCode . Grant . ApplicationID != app . ID {
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidGrant ,
ErrorDescription : "invalid grant" ,
} )
return
}
// remove token from database to deny duplicate usage
2022-05-20 16:08:52 +02:00
if err := authorizationCode . Invalidate ( ctx ) ; err != nil {
2019-03-08 17:42:50 +01:00
handleAccessTokenError ( ctx , AccessTokenError {
ErrorCode : AccessTokenErrorCodeInvalidRequest ,
ErrorDescription : "cannot proceed your request" ,
} )
}
2022-05-20 16:08:52 +02:00
resp , tokenErr := newAccessTokenResponse ( ctx , authorizationCode . Grant , serverKey , clientKey )
2019-03-08 17:42:50 +01:00
if tokenErr != nil {
handleAccessTokenError ( ctx , * tokenErr )
return
}
// send successful response
2021-04-05 17:30:52 +02:00
ctx . JSON ( http . StatusOK , resp )
2019-03-08 17:42:50 +01:00
}
func handleAccessTokenError ( ctx * context . Context , acErr AccessTokenError ) {
2021-04-05 17:30:52 +02:00
ctx . JSON ( http . StatusBadRequest , acErr )
2019-03-08 17:42:50 +01:00
}
2021-12-20 05:41:31 +01:00
func handleServerError ( ctx * context . Context , state , redirectURI string ) {
2019-03-08 17:42:50 +01:00
handleAuthorizeError ( ctx , AuthorizeError {
ErrorCode : ErrorCodeServerError ,
ErrorDescription : "A server error occurred" ,
State : state ,
} , redirectURI )
}
func handleAuthorizeError ( ctx * context . Context , authErr AuthorizeError , redirectURI string ) {
if redirectURI == "" {
log . Warn ( "Authorization failed: %v" , authErr . ErrorDescription )
ctx . Data [ "Error" ] = authErr
2022-03-23 05:54:07 +01:00
ctx . HTML ( http . StatusBadRequest , tplGrantError )
2019-03-08 17:42:50 +01:00
return
}
redirect , err := url . Parse ( redirectURI )
if err != nil {
ctx . ServerError ( "url.Parse" , err )
return
}
q := redirect . Query ( )
q . Set ( "error" , string ( authErr . ErrorCode ) )
q . Set ( "error_description" , authErr . ErrorDescription )
q . Set ( "state" , authErr . State )
redirect . RawQuery = q . Encode ( )
2022-03-23 05:54:07 +01:00
ctx . Redirect ( redirect . String ( ) , http . StatusSeeOther )
2019-03-08 17:42:50 +01:00
}
2022-01-02 14:12:35 +01:00
// SignInOAuth handles the OAuth2 login buttons
func SignInOAuth ( ctx * context . Context ) {
provider := ctx . Params ( ":provider" )
authSource , err := auth . GetActiveOAuth2SourceByName ( provider )
if err != nil {
ctx . ServerError ( "SignIn" , err )
return
}
2023-01-24 17:41:38 +01:00
redirectTo := ctx . FormString ( "redirect_to" )
if len ( redirectTo ) > 0 {
middleware . SetRedirectToCookie ( ctx . Resp , redirectTo )
}
2022-01-02 14:12:35 +01:00
// try to do a direct callback flow, so we don't authenticate the user again but use the valid accesstoken to get the user
user , gothUser , err := oAuth2UserLoginCallback ( authSource , ctx . Req , ctx . Resp )
if err == nil && user != nil {
// we got the user without going through the whole OAuth2 authentication flow again
handleOAuth2SignIn ( ctx , authSource , user , gothUser )
return
}
if err = authSource . Cfg . ( * oauth2 . Source ) . Callout ( ctx . Req , ctx . Resp ) ; err != nil {
if strings . Contains ( err . Error ( ) , "no provider for " ) {
if err = oauth2 . ResetOAuth2 ( ) ; err != nil {
ctx . ServerError ( "SignIn" , err )
return
}
if err = authSource . Cfg . ( * oauth2 . Source ) . Callout ( ctx . Req , ctx . Resp ) ; err != nil {
ctx . ServerError ( "SignIn" , err )
}
return
}
ctx . ServerError ( "SignIn" , err )
}
// redirect is done in oauth2.Auth
}
// SignInOAuthCallback handles the callback from the given provider
func SignInOAuthCallback ( ctx * context . Context ) {
provider := ctx . Params ( ":provider" )
// first look if the provider is still active
authSource , err := auth . GetActiveOAuth2SourceByName ( provider )
if err != nil {
ctx . ServerError ( "SignIn" , err )
return
}
if authSource == nil {
ctx . ServerError ( "SignIn" , errors . New ( "No valid provider found, check configured callback url in provider" ) )
return
}
u , gothUser , err := oAuth2UserLoginCallback ( authSource , ctx . Req , ctx . Resp )
if err != nil {
if user_model . IsErrUserProhibitLogin ( err ) {
2022-02-03 11:44:18 +01:00
uplerr := err . ( user_model . ErrUserProhibitLogin )
2022-01-02 14:12:35 +01:00
log . Info ( "Failed authentication attempt for %s from %s: %v" , uplerr . Name , ctx . RemoteAddr ( ) , err )
ctx . Data [ "Title" ] = ctx . Tr ( "auth.prohibit_login" )
ctx . HTML ( http . StatusOK , "user/auth/prohibit_login" )
return
}
2022-01-07 22:02:09 +01:00
if callbackErr , ok := err . ( errCallback ) ; ok {
log . Info ( "Failed OAuth callback: (%v) %v" , callbackErr . Code , callbackErr . Description )
switch callbackErr . Code {
case "access_denied" :
ctx . Flash . Error ( ctx . Tr ( "auth.oauth.signin.error.access_denied" ) )
case "temporarily_unavailable" :
ctx . Flash . Error ( ctx . Tr ( "auth.oauth.signin.error.temporarily_unavailable" ) )
default :
ctx . Flash . Error ( ctx . Tr ( "auth.oauth.signin.error" ) )
}
ctx . Redirect ( setting . AppSubURL + "/user/login" )
return
}
2022-01-02 14:12:35 +01:00
ctx . ServerError ( "UserSignIn" , err )
return
}
if u == nil {
2022-05-29 02:03:17 +02:00
if ctx . Doer != nil {
// attach user to already logged in user
err = externalaccount . LinkAccountToUser ( ctx . Doer , gothUser )
if err != nil {
ctx . ServerError ( "UserLinkAccount" , err )
return
}
ctx . Redirect ( setting . AppSubURL + "/user/settings/security" )
return
} else if ! setting . Service . AllowOnlyInternalRegistration && setting . OAuth2Client . EnableAutoRegistration {
2022-01-02 14:12:35 +01:00
// create new user with details from oauth2 provider
var missingFields [ ] string
if gothUser . UserID == "" {
missingFields = append ( missingFields , "sub" )
}
if gothUser . Email == "" {
missingFields = append ( missingFields , "email" )
}
if setting . OAuth2Client . Username == setting . OAuth2UsernameNickname && gothUser . NickName == "" {
missingFields = append ( missingFields , "nickname" )
}
if len ( missingFields ) > 0 {
log . Error ( "OAuth2 Provider %s returned empty or missing fields: %s" , authSource . Name , missingFields )
if authSource . IsOAuth2 ( ) && authSource . Cfg . ( * oauth2 . Source ) . Provider == "openidConnect" {
log . Error ( "You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields" )
}
err = fmt . Errorf ( "OAuth2 Provider %s returned empty or missing fields: %s" , authSource . Name , missingFields )
ctx . ServerError ( "CreateUser" , err )
return
}
u = & user_model . User {
2022-04-29 21:38:11 +02:00
Name : getUserName ( & gothUser ) ,
FullName : gothUser . Name ,
Email : gothUser . Email ,
LoginType : auth . OAuth2 ,
LoginSource : authSource . ID ,
LoginName : gothUser . UserID ,
}
overwriteDefault := & user_model . CreateUserOverwriteOptions {
IsActive : util . OptionalBoolOf ( ! setting . OAuth2Client . RegisterEmailConfirm ) ,
2022-01-02 14:12:35 +01:00
}
2023-02-08 07:44:42 +01:00
source := authSource . Cfg . ( * oauth2 . Source )
setUserAdminAndRestrictedFromGroupClaims ( source , u , & gothUser )
2022-01-02 14:12:35 +01:00
2022-04-29 21:38:11 +02:00
if ! createAndHandleCreatedUser ( ctx , base . TplName ( "" ) , nil , u , overwriteDefault , & gothUser , setting . OAuth2Client . AccountLinking != setting . OAuth2AccountLinkingDisabled ) {
2022-01-02 14:12:35 +01:00
// error already handled
return
}
2023-02-08 07:44:42 +01:00
if err := syncGroupsToTeams ( ctx , source , & gothUser , u ) ; err != nil {
ctx . ServerError ( "SyncGroupsToTeams" , err )
return
}
2022-01-02 14:12:35 +01:00
} else {
// no existing user is found, request attach or new account
showLinkingLogin ( ctx , gothUser )
return
}
}
handleOAuth2SignIn ( ctx , authSource , u , gothUser )
}
2023-02-08 07:44:42 +01:00
func claimValueToStringSet ( claimValue interface { } ) container . Set [ string ] {
2022-01-02 14:12:35 +01:00
var groups [ ] string
switch rawGroup := claimValue . ( type ) {
case [ ] string :
groups = rawGroup
2022-01-31 21:41:11 +01:00
case [ ] interface { } :
for _ , group := range rawGroup {
groups = append ( groups , fmt . Sprintf ( "%s" , group ) )
}
2022-01-02 14:12:35 +01:00
default :
str := fmt . Sprintf ( "%s" , rawGroup )
groups = strings . Split ( str , "," )
}
2023-02-08 07:44:42 +01:00
return container . SetOf ( groups ... )
2022-01-02 14:12:35 +01:00
}
2023-02-08 07:44:42 +01:00
func syncGroupsToTeams ( ctx * context . Context , source * oauth2 . Source , gothUser * goth . User , u * user_model . User ) error {
if source . GroupTeamMap != "" || source . GroupTeamMapRemoval {
groupTeamMapping , err := auth_module . UnmarshalGroupTeamMapping ( source . GroupTeamMap )
if err != nil {
return err
}
groups := getClaimedGroups ( source , gothUser )
if err := source_service . SyncGroupsToTeams ( ctx , u , groups , groupTeamMapping , source . GroupTeamMapRemoval ) ; err != nil {
return err
}
2022-01-02 14:12:35 +01:00
}
2023-02-08 07:44:42 +01:00
return nil
}
func getClaimedGroups ( source * oauth2 . Source , gothUser * goth . User ) container . Set [ string ] {
2022-01-02 14:12:35 +01:00
groupClaims , has := gothUser . RawData [ source . GroupClaimName ]
if ! has {
2023-02-08 07:44:42 +01:00
return nil
2022-01-02 14:12:35 +01:00
}
2023-02-08 07:44:42 +01:00
return claimValueToStringSet ( groupClaims )
}
func setUserAdminAndRestrictedFromGroupClaims ( source * oauth2 . Source , u * user_model . User , gothUser * goth . User ) bool {
groups := getClaimedGroups ( source , gothUser )
2022-01-02 14:12:35 +01:00
wasAdmin , wasRestricted := u . IsAdmin , u . IsRestricted
if source . AdminGroup != "" {
2023-02-08 07:44:42 +01:00
u . IsAdmin = groups . Contains ( source . AdminGroup )
2022-01-02 14:12:35 +01:00
}
if source . RestrictedGroup != "" {
2023-02-08 07:44:42 +01:00
u . IsRestricted = groups . Contains ( source . RestrictedGroup )
2022-01-02 14:12:35 +01:00
}
return wasAdmin != u . IsAdmin || wasRestricted != u . IsRestricted
}
func showLinkingLogin ( ctx * context . Context , gothUser goth . User ) {
2022-11-10 12:43:06 +01:00
if err := updateSession ( ctx , nil , map [ string ] interface { } {
"linkAccountGothUser" : gothUser ,
} ) ; err != nil {
ctx . ServerError ( "updateSession" , err )
2022-01-02 14:12:35 +01:00
return
}
ctx . Redirect ( setting . AppSubURL + "/user/link_account" )
}
func updateAvatarIfNeed ( url string , u * user_model . User ) {
if setting . OAuth2Client . UpdateAvatar && len ( url ) > 0 {
resp , err := http . Get ( url )
if err == nil {
defer func ( ) {
_ = resp . Body . Close ( )
} ( )
}
// ignore any error
if err == nil && resp . StatusCode == http . StatusOK {
data , err := io . ReadAll ( io . LimitReader ( resp . Body , setting . Avatar . MaxFileSize + 1 ) )
if err == nil && int64 ( len ( data ) ) <= setting . Avatar . MaxFileSize {
_ = user_service . UploadAvatar ( u , data )
}
}
}
}
func handleOAuth2SignIn ( ctx * context . Context , source * auth . Source , u * user_model . User , gothUser goth . User ) {
updateAvatarIfNeed ( gothUser . AvatarURL , u )
needs2FA := false
if ! source . Cfg . ( * oauth2 . Source ) . SkipLocalTwoFA {
_ , err := auth . GetTwoFactorByUID ( u . ID )
if err != nil && ! auth . IsErrTwoFactorNotEnrolled ( err ) {
ctx . ServerError ( "UserSignIn" , err )
return
}
needs2FA = err == nil
}
2023-02-08 07:44:42 +01:00
oauth2Source := source . Cfg . ( * oauth2 . Source )
groupTeamMapping , err := auth_module . UnmarshalGroupTeamMapping ( oauth2Source . GroupTeamMap )
if err != nil {
ctx . ServerError ( "UnmarshalGroupTeamMapping" , err )
return
}
groups := getClaimedGroups ( oauth2Source , & gothUser )
2022-01-02 14:12:35 +01:00
// If this user is enrolled in 2FA and this source doesn't override it,
// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
if ! needs2FA {
2022-11-10 12:43:06 +01:00
if err := updateSession ( ctx , nil , map [ string ] interface { } {
"uid" : u . ID ,
"uname" : u . Name ,
} ) ; err != nil {
ctx . ServerError ( "updateSession" , err )
2022-01-02 14:12:35 +01:00
return
}
2022-04-08 07:21:05 +02:00
// Clear whatever CSRF cookie has right now, force to generate a new one
2022-01-02 14:12:35 +01:00
middleware . DeleteCSRFCookie ( ctx . Resp )
// Register last login
u . SetLastLogin ( )
// Update GroupClaims
2023-02-08 07:44:42 +01:00
changed := setUserAdminAndRestrictedFromGroupClaims ( oauth2Source , u , & gothUser )
2022-01-02 14:12:35 +01:00
cols := [ ] string { "last_login_unix" }
if changed {
cols = append ( cols , "is_admin" , "is_restricted" )
}
2022-03-22 16:22:54 +01:00
if err := user_model . UpdateUserCols ( ctx , u , cols ... ) ; err != nil {
2022-01-02 14:12:35 +01:00
ctx . ServerError ( "UpdateUserCols" , err )
return
}
2023-02-08 07:44:42 +01:00
if oauth2Source . GroupTeamMap != "" || oauth2Source . GroupTeamMapRemoval {
if err := source_service . SyncGroupsToTeams ( ctx , u , groups , groupTeamMapping , oauth2Source . GroupTeamMapRemoval ) ; err != nil {
ctx . ServerError ( "SyncGroupsToTeams" , err )
return
}
}
2022-01-02 14:12:35 +01:00
// update external user information
if err := externalaccount . UpdateExternalUser ( u , gothUser ) ; err != nil {
2022-10-19 21:07:21 +02:00
if ! errors . Is ( err , util . ErrNotExist ) {
log . Error ( "UpdateExternalUser failed: %v" , err )
}
2022-01-02 14:12:35 +01:00
}
if err := resetLocale ( ctx , u ) ; err != nil {
ctx . ServerError ( "resetLocale" , err )
return
}
if redirectTo := ctx . GetCookie ( "redirect_to" ) ; len ( redirectTo ) > 0 {
middleware . DeleteRedirectToCookie ( ctx . Resp )
ctx . RedirectToFirst ( redirectTo )
return
}
ctx . Redirect ( setting . AppSubURL + "/" )
return
}
2023-02-08 07:44:42 +01:00
changed := setUserAdminAndRestrictedFromGroupClaims ( oauth2Source , u , & gothUser )
2022-01-02 14:12:35 +01:00
if changed {
2022-03-22 16:22:54 +01:00
if err := user_model . UpdateUserCols ( ctx , u , "is_admin" , "is_restricted" ) ; err != nil {
2022-01-02 14:12:35 +01:00
ctx . ServerError ( "UpdateUserCols" , err )
return
}
}
2023-02-08 07:44:42 +01:00
if oauth2Source . GroupTeamMap != "" || oauth2Source . GroupTeamMapRemoval {
if err := source_service . SyncGroupsToTeams ( ctx , u , groups , groupTeamMapping , oauth2Source . GroupTeamMapRemoval ) ; err != nil {
ctx . ServerError ( "SyncGroupsToTeams" , err )
return
}
}
2022-11-10 12:43:06 +01:00
if err := updateSession ( ctx , nil , map [ string ] interface { } {
// User needs to use 2FA, save data and redirect to 2FA page.
"twofaUid" : u . ID ,
"twofaRemember" : false ,
} ) ; err != nil {
ctx . ServerError ( "updateSession" , err )
2022-01-02 14:12:35 +01:00
return
}
2022-01-14 16:03:31 +01:00
// If WebAuthn is enrolled -> Redirect to WebAuthn instead
regs , err := auth . GetWebAuthnCredentialsByUID ( u . ID )
2022-01-02 14:12:35 +01:00
if err == nil && len ( regs ) > 0 {
2022-01-14 16:03:31 +01:00
ctx . Redirect ( setting . AppSubURL + "/user/webauthn" )
2022-01-02 14:12:35 +01:00
return
}
ctx . Redirect ( setting . AppSubURL + "/user/two_factor" )
}
// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
// login the user
func oAuth2UserLoginCallback ( authSource * auth . Source , request * http . Request , response http . ResponseWriter ) ( * user_model . User , goth . User , error ) {
oauth2Source := authSource . Cfg . ( * oauth2 . Source )
2022-06-20 17:37:54 +02:00
// Make sure that the response is not an error response.
errorName := request . FormValue ( "error" )
if len ( errorName ) > 0 {
errorDescription := request . FormValue ( "error_description" )
// Delete the goth session
err := gothic . Logout ( response , request )
if err != nil {
return nil , goth . User { } , err
}
return nil , goth . User { } , errCallback {
Code : errorName ,
Description : errorDescription ,
}
}
// Proceed to authenticate through goth.
2022-01-02 14:12:35 +01:00
gothUser , err := oauth2Source . Callback ( request , response )
if err != nil {
if err . Error ( ) == "securecookie: the value is too long" || strings . Contains ( err . Error ( ) , "Data too long" ) {
log . Error ( "OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider" , authSource . Name , setting . OAuth2 . MaxTokenLength )
err = fmt . Errorf ( "OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider" , authSource . Name , setting . OAuth2 . MaxTokenLength )
}
return nil , goth . User { } , err
}
if oauth2Source . RequiredClaimName != "" {
claimInterface , has := gothUser . RawData [ oauth2Source . RequiredClaimName ]
if ! has {
return nil , goth . User { } , user_model . ErrUserProhibitLogin { Name : gothUser . UserID }
}
if oauth2Source . RequiredClaimValue != "" {
2023-02-08 07:44:42 +01:00
groups := claimValueToStringSet ( claimInterface )
if ! groups . Contains ( oauth2Source . RequiredClaimValue ) {
2022-01-02 14:12:35 +01:00
return nil , goth . User { } , user_model . ErrUserProhibitLogin { Name : gothUser . UserID }
}
}
}
user := & user_model . User {
LoginName : gothUser . UserID ,
LoginType : auth . OAuth2 ,
LoginSource : authSource . ID ,
}
hasUser , err := user_model . GetUser ( user )
if err != nil {
return nil , goth . User { } , err
}
if hasUser {
return user , gothUser , nil
}
// search in external linked users
externalLoginUser := & user_model . ExternalLoginUser {
ExternalID : gothUser . UserID ,
LoginSourceID : authSource . ID ,
}
hasUser , err = user_model . GetExternalLogin ( externalLoginUser )
if err != nil {
return nil , goth . User { } , err
}
if hasUser {
2022-12-03 03:48:26 +01:00
user , err = user_model . GetUserByID ( request . Context ( ) , externalLoginUser . UserID )
2022-01-02 14:12:35 +01:00
return user , gothUser , err
}
// no user found to login
return nil , gothUser , nil
}