4343 ErrIdentityProvider = errors .New ("error from the identity provider" )
4444)
4545
46+ // extractSessionID extracts the session ID (sid) from an id_token.
47+ func extractSessionID (idToken string ) string {
48+ if idToken == "" {
49+ return ""
50+ }
51+ claims := jwt.MapClaims {}
52+ _ , _ , _ = jwt .NewParser ().ParseUnverified (idToken , claims )
53+ sid , _ := claims ["sid" ].(string )
54+ if sid != "" {
55+ logger .WithNamespace ("oidc" ).Debugf ("Extracted session ID from id_token: %s" , sid )
56+ }
57+ return sid
58+ }
59+
4660// Start is the route to start the OpenID Connect dance.
4761func Start (c echo.Context ) error {
4862 inst := middlewares .GetInstance (c )
@@ -195,12 +209,13 @@ func Redirect(c echo.Context) error {
195209 if err != nil {
196210 return renderError (c , nil , http .StatusBadRequest , "No OpenID Connect is configured." )
197211 }
198- accessToken , err := getToken (conf , c .QueryParam ("code" ))
212+ // Use getToken to get both access_token and id_token
213+ tokenResp , err := getToken (conf , c .QueryParam ("code" ))
199214 if err != nil {
200215 logger .WithNamespace ("oidc" ).Errorf ("Error on getToken: %s" , err )
201216 return renderError (c , nil , http .StatusBadGateway , "Error from the identity provider." )
202217 }
203- params , err := getUserInfo (conf , accessToken )
218+ params , err := getUserInfo (conf , tokenResp . AccessToken )
204219 if err != nil {
205220 return err
206221 }
@@ -220,7 +235,9 @@ func Redirect(c echo.Context) error {
220235 return renderError (c , nil , http .StatusNotFound , "Sorry, the twake was not found." )
221236 }
222237
223- code := getStorage ().CreateCode (sub )
238+ // Extract session ID from id_token for OIDC logout support
239+ sessionID := extractSessionID (tokenResp .IDToken )
240+ code := getStorage ().CreateCodeData (sub , sessionID )
224241 u , err := url .Parse (state .Redirect )
225242 if err != nil {
226243 return renderError (c , nil , http .StatusNotFound , "Sorry, an error occurred." )
@@ -307,11 +324,15 @@ func Login(c echo.Context) error {
307324 }
308325
309326 var token string
327+ // Track the id_token separately for SID extraction (used for OIDC logout)
328+ var idTokenForSID string
329+
310330 if idToken != "" && conf .IDTokenKeyURL != "" {
311331 if err := checkIDToken (conf , inst , idToken ); err != nil {
312332 return renderError (c , inst , http .StatusBadRequest , err .Error ())
313333 }
314334 token = idToken
335+ idTokenForSID = idToken
315336 } else {
316337 if conf .AllowOAuthToken {
317338 token = c .QueryParam ("access_token" )
@@ -321,11 +342,14 @@ func Login(c echo.Context) error {
321342 return renderError (c , nil , http .StatusNotFound , "Sorry, the session has expired." )
322343 }
323344 code := c .QueryParam ("code" )
324- token , err = getToken (conf , code )
345+ // Use getToken to get both access_token and id_token
346+ tokenResp , err := getToken (conf , code )
325347 if err != nil {
326348 logger .WithNamespace ("oidc" ).Errorf ("Error on getToken: %s" , err )
327349 return renderError (c , inst , http .StatusBadGateway , "Error from the identity provider." )
328350 }
351+ token = tokenResp .AccessToken
352+ idTokenForSID = tokenResp .IDToken
329353 if state .SharingID != "" {
330354 return acceptSharing (c , inst , conf , token , state )
331355 }
@@ -347,9 +371,8 @@ func Login(c echo.Context) error {
347371 }
348372 }
349373
350- claims := jwt.MapClaims {}
351- _ , _ , _ = jwt .NewParser ().ParseUnverified (token , claims )
352- sid , _ := claims ["sid" ].(string )
374+ // Extract SID from id_token (not access_token) for OIDC logout support
375+ sid := extractSessionID (idTokenForSID )
353376 return createSessionAndRedirect (c , inst , redirect , confirm , sid )
354377}
355378
@@ -548,6 +571,39 @@ func Logout(c echo.Context) error {
548571 return c .NoContent (http .StatusOK )
549572}
550573
574+ // validateDelegatedCode validates a delegated code and returns the associated session ID.
575+ // Returns an error if the code is invalid or doesn't match the instance.
576+ func validateDelegatedCode (inst * instance.Instance , code string ) (string , error ) {
577+ codeData := getStorage ().GetCodeData (code )
578+ if codeData == nil || codeData .Sub == "" {
579+ return "" , errors .New ("invalid code" )
580+ }
581+
582+ sub := codeData .Sub
583+ if sub != inst .OIDCID && sub != inst .FranceConnectID && sub != inst .Domain {
584+ inst .Logger ().WithNamespace ("oidc" ).Infof ("AccessToken invalid code: %s (%s - %s - %s)" ,
585+ sub , inst .OIDCID , inst .FranceConnectID , inst .Domain )
586+ return "" , errors .New ("invalid code" )
587+ }
588+
589+ return codeData .SessionID , nil
590+ }
591+
592+ // validateOIDCToken validates an OIDC token (id_token or access_token) for the instance.
593+ func validateOIDCToken (inst * instance.Instance , idToken , oidcToken string ) error {
594+ conf , err := getGenericConfig (inst .ContextName )
595+ if err != nil || ! conf .AllowOAuthToken {
596+ return errors .New ("this endpoint is not enabled" )
597+ }
598+
599+ if idToken != "" {
600+ err = checkIDToken (conf , inst , idToken )
601+ } else {
602+ err = checkDomainFromUserInfo (conf , inst , oidcToken )
603+ }
604+ return err
605+ }
606+
551607// AccessToken delivers an access_token and a refresh_token if the client gives
552608// a valid token for OIDC.
553609func AccessToken (c echo.Context ) error {
@@ -566,33 +622,18 @@ func AccessToken(c echo.Context) error {
566622 return err
567623 }
568624
625+ // Validate the delegated code or OIDC token
626+ var codeSessionID string
569627 if reqBody .Code != "" {
570- sub := getStorage ().GetSub (reqBody .Code )
571- invalidCode := sub == ""
572- if sub != inst .OIDCID && sub != inst .FranceConnectID && sub != inst .Domain {
573- invalidCode = true
574- }
575- if invalidCode {
576- inst .Logger ().WithNamespace ("oidc" ).Infof ("AccessToken invalid code: %s (%s - %s - %s)" ,
577- sub , inst .OIDCID , inst .FranceConnectID , inst .Domain )
628+ sessionID , err := validateDelegatedCode (inst , reqBody .Code )
629+ if err != nil {
578630 return c .JSON (http .StatusBadRequest , echo.Map {
579631 "error" : "invalid code" ,
580632 })
581633 }
634+ codeSessionID = sessionID
582635 } else {
583- conf , err := getGenericConfig (inst .ContextName )
584- if err != nil || ! conf .AllowOAuthToken {
585- return c .JSON (http .StatusBadRequest , echo.Map {
586- "error" : "this endpoint is not enabled" ,
587- })
588- }
589- // Check the token from the remote URL.
590- if reqBody .IDToken != "" {
591- err = checkIDToken (conf , inst , reqBody .IDToken )
592- } else {
593- err = checkDomainFromUserInfo (conf , inst , reqBody .OIDCToken )
594- }
595- if err != nil {
636+ if err := validateOIDCToken (inst , reqBody .IDToken , reqBody .OIDCToken ); err != nil {
596637 return c .JSON (http .StatusBadRequest , echo.Map {
597638 "error" : err .Error (),
598639 })
@@ -658,27 +699,19 @@ func AccessToken(c echo.Context) error {
658699 }
659700 }
660701
661- // Store the session ID in the client for logout purposes
662- if client .Flagship {
663- if reqBody .IDToken != "" {
664- // Extract SID from the ID token
665- claims := jwt.MapClaims {}
666- _ , _ , _ = jwt .NewParser ().ParseUnverified (reqBody .IDToken , claims )
667- if sid , ok := claims ["sid" ].(string ); ok && sid != "" {
668- client .OIDCSessionID = sid
669- } else {
670- logger .WithNamespace ("oidc" ).Warnf ("No session ID found in ID token" )
671- return c .JSON (http .StatusBadRequest , echo.Map {
672- "error" : "No session ID found in ID token" ,
673- })
674- }
675- } else {
676- logger .WithNamespace ("oidc" ).Warnf ("No ID token Logout won't work" )
677- }
702+ // Store the session ID in the client for logout purposes (priority: delegated code > id_token)
703+ sessionID := codeSessionID
704+ if sessionID == "" {
705+ sessionID = extractSessionID (reqBody .IDToken )
706+ }
707+ if sessionID != "" {
708+ client .OIDCSessionID = sessionID
709+ logger .WithNamespace ("oidc" ).Debugf ("Using session ID for OIDC logout: %s" , sessionID )
678710 }
679711
680- // Remove the pending flag on the OAuth client (if needed)
681- if client .Pending {
712+ // Update the OAuth client if needed (pending flag or new session ID)
713+ needsUpdate := client .Pending || sessionID != ""
714+ if needsUpdate {
682715 client .Pending = false
683716 client .ClientID = ""
684717 _ = couchdb .UpdateDoc (inst , client )
@@ -907,15 +940,7 @@ type tokenResponse struct {
907940 IDToken string `json:"id_token"`
908941}
909942
910- func getToken (conf * Config , code string ) (string , error ) {
911- resp , err := getTokenWithIDToken (conf , code )
912- if err != nil {
913- return "" , err
914- }
915- return resp .AccessToken , nil
916- }
917-
918- func getTokenWithIDToken (conf * Config , code string ) (* tokenResponse , error ) {
943+ func getToken (conf * Config , code string ) (* tokenResponse , error ) {
919944 data := url.Values {
920945 "grant_type" : []string {"authorization_code" },
921946 "code" : []string {code },
@@ -1251,7 +1276,8 @@ func Routes(router *echo.Group) {
12511276
12521277// GetDelegatedCode is mostly a proxy for the userinfo request made by the
12531278// cloudery to the OIDC provider. It adds a delegated code in the response
1254- // associated to the sub.
1279+ // associated to the sub. If an id_token is provided, the session ID (sid) is
1280+ // extracted and stored with the delegated code for later use in OIDC logout.
12551281func GetDelegatedCode (c echo.Context ) error {
12561282 contextName := c .Param ("context" )
12571283 provider := c .Param ("provider" )
@@ -1270,6 +1296,7 @@ func GetDelegatedCode(c echo.Context) error {
12701296
12711297 var reqBody struct {
12721298 AccessToken string `json:"access_token"`
1299+ IDToken string `json:"id_token"`
12731300 }
12741301 if err := c .Bind (& reqBody ); err != nil {
12751302 return err
@@ -1297,8 +1324,11 @@ func GetDelegatedCode(c echo.Context) error {
12971324 s = domain
12981325 }
12991326
1327+ // Extract session ID (sid) from id_token if provided
1328+ sessionID := extractSessionID (reqBody .IDToken )
1329+
13001330 logger .WithNamespace ("oidc" ).Infof ("GetDelegatedCode for %s" , s )
1301- params ["delegated_code" ] = getStorage ().CreateCode ( s )
1331+ params ["delegated_code" ] = getStorage ().CreateCodeData ( s , sessionID )
13021332 return c .JSON (http .StatusOK , params )
13031333}
13041334
0 commit comments