88 "fmt"
99 "io"
1010 "net/http"
11+ "net/url"
12+ "strings"
1113 "time"
1214
1315 "github.com/gin-gonic/gin"
@@ -23,11 +25,6 @@ import (
2325)
2426
2527func NewOIDC (conf * config.Configuration , db * database.GormDatabase , userChangeNotifier * UserChangeNotifier ) * OIDCAPI {
26- scopes := conf .OIDC .Scopes
27- if len (scopes ) == 0 {
28- scopes = []string {"openid" , "profile" , "email" }
29- }
30-
3128 cookieKey := make ([]byte , 32 )
3229 if _ , err := rand .Read (cookieKey ); err != nil {
3330 log .Fatal ().Err (err ).Msg ("failed to generate OIDC cookie key" )
@@ -38,15 +35,19 @@ func NewOIDC(conf *config.Configuration, db *database.GormDatabase, userChangeNo
3835 }
3936 cookieHandler := httphelper .NewCookieHandler (cookieKey , cookieKey , cookieHandlerOpt ... )
4037
41- opts := []rp.Option {rp .WithCookieHandler (cookieHandler ), rp .WithPKCE (cookieHandler )}
38+ opts := []rp.Option {
39+ rp .WithCookieHandler (cookieHandler ),
40+ rp .WithPKCE (cookieHandler ),
41+ rp .WithSigningAlgsFromDiscovery (),
42+ }
4243
4344 provider , err := rp .NewRelyingPartyOIDC (
4445 context .Background (),
4546 conf .OIDC .Issuer ,
4647 conf .OIDC .ClientID ,
4748 conf .OIDC .ClientSecret ,
4849 conf .OIDC .RedirectURL ,
49- scopes ,
50+ conf . OIDC . Scopes ,
5051 opts ... ,
5152 )
5253 if err != nil {
@@ -61,6 +62,7 @@ func NewOIDC(conf *config.Configuration, db *database.GormDatabase, userChangeNo
6162 PasswordStrength : conf .PassStrength ,
6263 SecureCookie : conf .Server .SecureCookie ,
6364 AutoRegister : conf .OIDC .AutoRegister ,
65+ LinkByUsername : conf .OIDC .LinkByUsername ,
6466 pendingSessions : decaymap .NewDecayMap [string , * pendingOIDCSession ](time .Now (), pendingSessionMaxAge ),
6567 }
6668}
@@ -88,6 +90,7 @@ type OIDCAPI struct {
8890 PasswordStrength int
8991 SecureCookie bool
9092 AutoRegister bool
93+ LinkByUsername bool
9194 pendingSessions * decaymap.DecayMap [string , * pendingOIDCSession ]
9295}
9396
@@ -201,7 +204,7 @@ func (a *OIDCAPI) ElevateHandler(ctx *gin.Context) {
201204// $ref: "#/definitions/Error"
202205func (a * OIDCAPI ) CallbackHandler () gin.HandlerFunc {
203206 callback := func (w http.ResponseWriter , r * http.Request , tokens * oidc.Tokens [* oidc.IDTokenClaims ], state string , provider rp.RelyingParty , info * oidc.UserInfo ) {
204- user , status , err := a .resolveUser (info )
207+ user , status , err := a .resolveUser (tokens . IDTokenClaims . GetIssuer (), info )
205208 if err != nil {
206209 http .Error (w , err .Error (), status )
207210 return
@@ -367,7 +370,7 @@ func (a *OIDCAPI) ExternalTokenHandler(ctx *gin.Context) {
367370 ctx .AbortWithError (http .StatusInternalServerError , fmt .Errorf ("failed to get user info: %w" , err ))
368371 return
369372 }
370- user , status , resolveErr := a .resolveUser (info )
373+ user , status , resolveErr := a .resolveUser (tokens . IDTokenClaims . GetIssuer (), info )
371374 if resolveErr != nil {
372375 ctx .AbortWithError (status , resolveErr )
373376 return
@@ -391,32 +394,83 @@ func (a *OIDCAPI) generateState() (string, error) {
391394 return hex .EncodeToString (nonce ), nil
392395}
393396
394- // resolveUser looks up or creates a user from OIDC userinfo claims.
395- func (a * OIDCAPI ) resolveUser (info * oidc.UserInfo ) (* model.User , int , error ) {
397+ // resolveUser looks up, links, or creates the user bound to an OIDC identity.
398+ //
399+ // 1. Look up the user by OIDC id (<iss>#<sub>). If found, use it.
400+ // 2. Otherwise look up a user by the username claim. If one exists, link it to
401+ // this OIDC identity, which requires GOTIFY_OIDC_LINK_BY_USERNAME and
402+ // that the user is not already bound to a different identity.
403+ // 3. Otherwise auto-register a new user, which requires GOTIFY_OIDC_AUTOREGISTER.
404+ func (a * OIDCAPI ) resolveUser (issuer string , info * oidc.UserInfo ) (* model.User , int , error ) {
405+ if issuer == "" {
406+ return nil , http .StatusInternalServerError , errors .New ("issuer claim was empty" )
407+ }
408+ if _ , err := url .Parse (issuer ); err != nil {
409+ return nil , http .StatusInternalServerError , fmt .Errorf ("issuer url %q is not a valid url: %w" , issuer , err )
410+ }
411+ if strings .Contains (issuer , "#" ) {
412+ return nil , http .StatusInternalServerError , fmt .Errorf ("issuer url %q may not contain a fragment" , issuer )
413+ }
414+ subject := info .GetSubject ()
415+ if subject == "" {
416+ return nil , http .StatusInternalServerError , errors .New ("subject claim was empty" )
417+ }
418+ oidcID := issuer + "#" + subject
419+
420+ user , err := a .DB .GetUserByOIDC (oidcID )
421+ if err != nil {
422+ return nil , http .StatusInternalServerError , fmt .Errorf ("database error: %w" , err )
423+ }
424+ if user != nil {
425+ return user , 0 , nil
426+ }
427+
396428 usernameRaw , ok := info .Claims [a .UsernameClaim ]
397429 if ! ok {
398430 return nil , http .StatusInternalServerError , fmt .Errorf ("username claim %q is missing" , a .UsernameClaim )
399431 }
400432 username := fmt .Sprint (usernameRaw )
401433 if username == "" || usernameRaw == nil {
402- return nil , http .StatusInternalServerError , fmt . Errorf ("username claim was empty" )
434+ return nil , http .StatusInternalServerError , errors . New ("username claim was empty" )
403435 }
404436
405- user , err := a .DB .GetUserByName (username )
437+ byUsername , err := a .DB .GetUserByName (username )
406438 if err != nil {
407439 return nil , http .StatusInternalServerError , fmt .Errorf ("database error: %w" , err )
408440 }
409- if user == nil {
410- if ! a .AutoRegister {
411- return nil , http .StatusForbidden , fmt .Errorf ("user does not exist and auto-registration is disabled" )
412- }
413- user = & model.User {Name : username , Admin : false , Pass : nil }
414- if err := a .DB .CreateUser (user ); err != nil {
415- return nil , http .StatusInternalServerError , fmt .Errorf ("failed to create user: %w" , err )
416- }
417- if err := a .UserChangeNotifier .fireUserAdded (user .ID ); err != nil {
418- log .Error ().Err (err ).Uint ("user_id" , user .ID ).Msg ("Could not notify user change" )
419- }
441+ if byUsername != nil {
442+ return a .linkExistingUser (byUsername , oidcID )
443+ }
444+ return a .registerUser (username , oidcID )
445+ }
446+
447+ func (a * OIDCAPI ) linkExistingUser (user * model.User , oidcID string ) (* model.User , int , error ) {
448+ if ! a .LinkByUsername {
449+ log .Warn ().Str ("oidc_id" , oidcID ).Str ("username" , user .Name ).Msgf ("OIDC login rejected: a local user with the username already exists and %s is disabled" , config .EnvOIDCLinkByUsername )
450+ return nil , http .StatusForbidden , fmt .Errorf ("a local user with the username %s already exists and linking by username is disabled" , user .Name )
451+ }
452+ if user .OIDCID != nil {
453+ log .Warn ().Str ("oidc_id" , oidcID ).Str ("bound_oidc_id" , * user .OIDCID ).Str ("username" , user .Name ).Msg ("OIDC login rejected: the username is already bound to a different OIDC identity" )
454+ return nil , http .StatusForbidden , fmt .Errorf ("the user %s is already bound to a different OIDC identity" , user .Name )
455+ }
456+ user .OIDCID = & oidcID
457+ if err := a .DB .UpdateUser (user ); err != nil {
458+ return nil , http .StatusInternalServerError , fmt .Errorf ("failed to bind user to OIDC identity: %w" , err )
459+ }
460+ return user , 0 , nil
461+ }
462+
463+ func (a * OIDCAPI ) registerUser (username , oidcID string ) (* model.User , int , error ) {
464+ if ! a .AutoRegister {
465+ return nil , http .StatusForbidden , errors .New ("user does not exist and auto-registration is disabled" )
466+ }
467+ user := & model.User {Name : username , Admin : false , Pass : nil , OIDCID : & oidcID }
468+ if err := a .DB .CreateUser (user ); err != nil {
469+ return nil , http .StatusInternalServerError , fmt .Errorf ("failed to create user: %w" , err )
470+ }
471+ log .Info ().Str ("oidc_id" , oidcID ).Str ("username" , user .Name ).Msg ("OIDC auto registration" )
472+ if err := a .UserChangeNotifier .fireUserAdded (user .ID ); err != nil {
473+ log .Error ().Err (err ).Uint ("user_id" , user .ID ).Msg ("Could not notify user change" )
420474 }
421475 return user , 0 , nil
422476}
0 commit comments