zitadel-oidc/pkg/op/session.go
Tim Möhlmann b8e520afd0
fix: allow expired ID token hint to end sessions (#522)
* fix: allow expired ID token hint to end sessions

This change adds a specific error for expired ID Token hints, including too old "issued at" and "max auth age".
The error is returned VerifyIDTokenHint so that the end session handler can choose to ignore this error.

This fixes the behavior to be in line with [OpenID Connect RP-Initiated Logout 1.0, section 4](https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ValidationAndErrorHandling).

* Tes IDTokenHintExpiredError
2024-01-19 11:30:51 +01:00

123 lines
3.9 KiB
Go

package op
import (
"context"
"errors"
"net/http"
"net/url"
"path"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
"golang.org/x/exp/slog"
)
type SessionEnder interface {
Decoder() httphelper.Decoder
Storage() Storage
IDTokenHintVerifier(context.Context) *IDTokenHintVerifier
DefaultLogoutRedirectURI() string
Logger() *slog.Logger
}
func endSessionHandler(ender SessionEnder) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
EndSession(w, r, ender)
}
}
func EndSession(w http.ResponseWriter, r *http.Request, ender SessionEnder) {
req, err := ParseEndSessionRequest(r, ender.Decoder())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session, err := ValidateEndSessionRequest(r.Context(), req, ender)
if err != nil {
RequestError(w, r, err, ender.Logger())
return
}
redirect := session.RedirectURI
if fromRequest, ok := ender.Storage().(CanTerminateSessionFromRequest); ok {
redirect, err = fromRequest.TerminateSessionFromRequest(r.Context(), session)
} else {
err = ender.Storage().TerminateSession(r.Context(), session.UserID, session.ClientID)
}
if err != nil {
RequestError(w, r, oidc.DefaultToServerError(err, "error terminating session"), ender.Logger())
return
}
http.Redirect(w, r, redirect, http.StatusFound)
}
func ParseEndSessionRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.EndSessionRequest, error) {
err := r.ParseForm()
if err != nil {
return nil, oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err)
}
req := new(oidc.EndSessionRequest)
err = decoder.Decode(req, r.Form)
if err != nil {
return nil, oidc.ErrInvalidRequest().WithDescription("error decoding form").WithParent(err)
}
return req, nil
}
func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest, ender SessionEnder) (*EndSessionRequest, error) {
session := &EndSessionRequest{
RedirectURI: ender.DefaultLogoutRedirectURI(),
}
if req.IdTokenHint != "" {
claims, err := VerifyIDTokenHint[*oidc.IDTokenClaims](ctx, req.IdTokenHint, ender.IDTokenHintVerifier(ctx))
if err != nil && !errors.As(err, &IDTokenHintExpiredError{}) {
return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err)
}
session.UserID = claims.GetSubject()
session.IDTokenHintClaims = claims
if req.ClientID != "" && req.ClientID != claims.GetAuthorizedParty() {
return nil, oidc.ErrInvalidRequest().WithDescription("client_id does not match azp of id_token_hint")
}
req.ClientID = claims.GetAuthorizedParty()
}
if req.ClientID != "" {
client, err := ender.Storage().GetClientByClientID(ctx, req.ClientID)
if err != nil {
return nil, oidc.DefaultToServerError(err, "")
}
session.ClientID = client.GetID()
if req.PostLogoutRedirectURI != "" {
if err := ValidateEndSessionPostLogoutRedirectURI(req.PostLogoutRedirectURI, client); err != nil {
return nil, err
}
session.RedirectURI = req.PostLogoutRedirectURI
}
}
if req.State != "" {
redirect, err := url.Parse(session.RedirectURI)
if err != nil {
return nil, oidc.DefaultToServerError(err, "")
}
session.RedirectURI = mergeQueryParams(redirect, url.Values{"state": {req.State}})
}
return session, nil
}
func ValidateEndSessionPostLogoutRedirectURI(postLogoutRedirectURI string, client Client) error {
for _, uri := range client.PostLogoutRedirectURIs() {
if uri == postLogoutRedirectURI {
return nil
}
}
if globClient, ok := client.(HasRedirectGlobs); ok {
for _, uriGlob := range globClient.PostLogoutRedirectURIGlobs() {
isMatch, err := path.Match(uriGlob, postLogoutRedirectURI)
if err != nil {
return oidc.ErrServerError().WithParent(err)
}
if isMatch {
return nil
}
}
}
return oidc.ErrInvalidRequest().WithDescription("post_logout_redirect_uri invalid")
}