mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-20 03:31:27 +02:00
154 lines
3.9 KiB
Go
154 lines
3.9 KiB
Go
package oidc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/lestrrat-go/jwx/v3/jwt"
|
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type endSessionService struct {
|
|
db *gorm.DB
|
|
store *Store
|
|
signer TokenSigner
|
|
baseURL string
|
|
}
|
|
|
|
func newEndSessionService(db *gorm.DB, store *Store, signer TokenSigner, baseURL string) *endSessionService {
|
|
return &endSessionService{
|
|
db: db,
|
|
store: store,
|
|
signer: signer,
|
|
baseURL: baseURL,
|
|
}
|
|
}
|
|
|
|
// endSession revokes the sessions belonging to the ID token hint and returns the
|
|
// client's post-logout callback URL (empty if none is configured).
|
|
func (s *endSessionService) endSession(ctx context.Context, input dto.OidcLogoutDto, userID string) (string, error) {
|
|
if input.IdTokenHint == "" {
|
|
return "", &common.TokenInvalidError{}
|
|
}
|
|
|
|
token, err := s.verifyIDTokenHint(input.IdTokenHint)
|
|
if err != nil {
|
|
return "", &common.TokenInvalidError{}
|
|
}
|
|
|
|
clientIDs, ok := token.Audience()
|
|
if !ok || len(clientIDs) == 0 {
|
|
return "", &common.TokenInvalidError{}
|
|
}
|
|
clientID := clientIDs[0]
|
|
if input.ClientId != "" && clientID != input.ClientId {
|
|
return "", &common.OidcClientIdNotMatchingError{}
|
|
}
|
|
|
|
subject, ok := token.Subject()
|
|
if !ok || subject == "" {
|
|
return "", &common.TokenInvalidError{}
|
|
}
|
|
if userID != "" && subject != userID {
|
|
return "", &common.TokenInvalidError{}
|
|
}
|
|
userID = subject
|
|
|
|
idTokenJTI, ok := token.JwtID()
|
|
if !ok {
|
|
return "", &common.TokenInvalidError{}
|
|
}
|
|
|
|
var callbackURL string
|
|
err = withTx(ctx, s.db, func(ctx context.Context) error {
|
|
var authorizedClient model.UserAuthorizedOidcClient
|
|
err := dbFromContext(ctx, s.db).
|
|
Preload("Client").
|
|
First(&authorizedClient, "client_id = ? AND user_id = ?", clientID, userID).
|
|
Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return &common.OidcMissingAuthorizationError{}
|
|
}
|
|
return err
|
|
}
|
|
|
|
callbackURL, err = logoutCallbackURL(&authorizedClient.Client, input.PostLogoutRedirectUri)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.store.RevokeSessionsByIDTokenHint(ctx, userID, clientID, idTokenJTI)
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return callbackURL, nil
|
|
}
|
|
|
|
func (s *endSessionService) verifyIDTokenHint(tokenString string) (jwt.Token, error) {
|
|
alg, err := s.signer.GetKeyAlg()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
token, err := jwt.ParseString(
|
|
tokenString,
|
|
jwt.WithValidate(true),
|
|
jwt.WithKey(alg, s.signer.GetPrivateKey()),
|
|
jwt.WithAcceptableSkew(time.Minute),
|
|
jwt.WithResetValidators(true),
|
|
jwt.WithIssuer(s.baseURL),
|
|
jwt.WithValidator(jwt.IsIssuedAtValid()),
|
|
jwt.WithValidator(jwt.IsNbfValid()),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// id_token_hint must be an ID token, never an access token (both are signed with the same
|
|
// key). An expired ID token is still accepted here, as required by OIDC RP-Initiated Logout.
|
|
var tokenType string
|
|
if err := token.Get(common.TokenTypeClaim, &tokenType); err != nil || tokenType != idTokenType {
|
|
return nil, &common.TokenInvalidError{}
|
|
}
|
|
|
|
return token, nil
|
|
}
|
|
|
|
func logoutCallbackURL(client *model.OidcClient, inputLogoutCallbackURL string) (string, error) {
|
|
if len(client.LogoutCallbackURLs) == 0 {
|
|
return "", nil
|
|
}
|
|
if inputLogoutCallbackURL == "" {
|
|
return client.LogoutCallbackURLs[0], nil
|
|
}
|
|
|
|
matched, err := utils.GetCallbackURLFromList(client.LogoutCallbackURLs, inputLogoutCallbackURL)
|
|
if err != nil || matched == "" {
|
|
return "", &common.OidcInvalidCallbackURLError{}
|
|
}
|
|
|
|
return matched, nil
|
|
}
|
|
|
|
func appendStateToURL(callbackURL string, state string) string {
|
|
parsed, err := url.Parse(callbackURL)
|
|
if err != nil {
|
|
return callbackURL
|
|
}
|
|
if state != "" {
|
|
q := parsed.Query()
|
|
q.Set("state", state)
|
|
parsed.RawQuery = q.Encode()
|
|
}
|
|
return parsed.String()
|
|
}
|