diff --git a/pkg/op/server.go b/pkg/op/server.go index 90bc664..d2419dd 100644 --- a/pkg/op/server.go +++ b/pkg/op/server.go @@ -69,6 +69,11 @@ type Server interface { // The recommended Response Data type is [oidc.DiscoveryConfiguration]. Discovery(context.Context, *Request[struct{}]) (*Response, error) + // Keys serves the JWK set which the client can use verify signatures from the op. + // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata `jwks_uri` key. + // The recommended Response Data type is [jose.JSOMWebKeySet]. + Keys(context.Context, *Request[struct{}]) (*Response, error) + // Authorize initiates the authorization flow and redirects to a login page. // See the various https://openid.net/specs/openid-connect-core-1_0.html // authorize endpoint sections (one for each type of flow). @@ -147,17 +152,12 @@ type Server interface { // Revocation handles token revocation using an access or refresh token. // https://datatracker.ietf.org/doc/html/rfc7009 // There are no response requirements. Data may remain empty. - Revocation(context.Context, *Request[oidc.RevocationRequest]) (*Response, error) + Revocation(context.Context, *ClientRequest[oidc.RevocationRequest]) (*Response, error) // EndSession handles the OpenID Connect RP-Initiated Logout. // https://openid.net/specs/openid-connect-rpinitiated-1_0.html // There are no response requirements. Data may remain empty. - EndSession(context.Context, *Request[oidc.EndSessionRequest]) (*Response, error) - - // Keys serves the JWK set which the client can use verify signatures from the op. - // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata `jwks_uri` key. - // The recommended Response Data type is [jose.JSOMWebKeySet]. - Keys(context.Context, *Request[struct{}]) (*Response, error) + EndSession(context.Context, *Request[oidc.EndSessionRequest]) (*Redirect, error) // mustImpl forces implementations to embed the UnimplementedServer for forward // compatibilty with the interface. @@ -268,70 +268,70 @@ func unimplementedGrantError(gt oidc.GrantType) StatusError { func (UnimplementedServer) mustImpl() {} -func (UnimplementedServer) Health(_ context.Context, r *Request[struct{}]) (*Response, error) { +func (UnimplementedServer) Health(ctx context.Context, r *Request[struct{}]) (*Response, error) { return nil, unimplementedError(r) } -func (UnimplementedServer) Ready(_ context.Context, r *Request[struct{}]) (*Response, error) { +func (UnimplementedServer) Ready(ctx context.Context, r *Request[struct{}]) (*Response, error) { return nil, unimplementedError(r) } -func (UnimplementedServer) Discovery(_ context.Context, r *Request[struct{}]) (*Response, error) { +func (UnimplementedServer) Discovery(ctx context.Context, r *Request[struct{}]) (*Response, error) { return nil, unimplementedError(r) } -func (UnimplementedServer) Authorize(_ context.Context, r *Request[oidc.AuthRequest]) (*Redirect, error) { +func (UnimplementedServer) Keys(ctx context.Context, r *Request[struct{}]) (*Response, error) { return nil, unimplementedError(r) } -func (UnimplementedServer) DeviceAuthorization(_ context.Context, r *ClientRequest[oidc.DeviceAuthorizationRequest]) (*Response, error) { +func (UnimplementedServer) Authorize(ctx context.Context, r *Request[oidc.AuthRequest]) (*Redirect, error) { + return nil, unimplementedError(r) +} + +func (UnimplementedServer) DeviceAuthorization(ctx context.Context, r *ClientRequest[oidc.DeviceAuthorizationRequest]) (*Response, error) { return nil, unimplementedError(r.Request) } -func (UnimplementedServer) VerifyClient(_ context.Context, r *Request[ClientCredentials]) (Client, error) { +func (UnimplementedServer) VerifyClient(ctx context.Context, r *Request[ClientCredentials]) (Client, error) { return nil, unimplementedError(r) } -func (UnimplementedServer) CodeExchange(_ context.Context, r *ClientRequest[oidc.AccessTokenRequest]) (*Response, error) { +func (UnimplementedServer) CodeExchange(ctx context.Context, r *ClientRequest[oidc.AccessTokenRequest]) (*Response, error) { return nil, unimplementedGrantError(oidc.GrantTypeCode) } -func (UnimplementedServer) RefreshToken(_ context.Context, r *ClientRequest[oidc.RefreshTokenRequest]) (*Response, error) { +func (UnimplementedServer) RefreshToken(ctx context.Context, r *ClientRequest[oidc.RefreshTokenRequest]) (*Response, error) { return nil, unimplementedGrantError(oidc.GrantTypeRefreshToken) } -func (UnimplementedServer) JWTProfile(_ context.Context, r *Request[oidc.JWTProfileGrantRequest]) (*Response, error) { +func (UnimplementedServer) JWTProfile(ctx context.Context, r *Request[oidc.JWTProfileGrantRequest]) (*Response, error) { return nil, unimplementedGrantError(oidc.GrantTypeBearer) } -func (UnimplementedServer) TokenExchange(_ context.Context, r *ClientRequest[oidc.TokenExchangeRequest]) (*Response, error) { +func (UnimplementedServer) TokenExchange(ctx context.Context, r *ClientRequest[oidc.TokenExchangeRequest]) (*Response, error) { return nil, unimplementedGrantError(oidc.GrantTypeTokenExchange) } -func (UnimplementedServer) ClientCredentialsExchange(_ context.Context, r *ClientRequest[oidc.ClientCredentialsRequest]) (*Response, error) { +func (UnimplementedServer) ClientCredentialsExchange(ctx context.Context, r *ClientRequest[oidc.ClientCredentialsRequest]) (*Response, error) { return nil, unimplementedGrantError(oidc.GrantTypeClientCredentials) } -func (UnimplementedServer) DeviceToken(_ context.Context, r *ClientRequest[oidc.DeviceAccessTokenRequest]) (*Response, error) { +func (UnimplementedServer) DeviceToken(ctx context.Context, r *ClientRequest[oidc.DeviceAccessTokenRequest]) (*Response, error) { return nil, unimplementedGrantError(oidc.GrantTypeDeviceCode) } -func (UnimplementedServer) Introspect(_ context.Context, r *ClientRequest[oidc.IntrospectionRequest]) (*Response, error) { +func (UnimplementedServer) Introspect(ctx context.Context, r *ClientRequest[oidc.IntrospectionRequest]) (*Response, error) { return nil, unimplementedError(r.Request) } -func (UnimplementedServer) UserInfo(_ context.Context, r *Request[oidc.UserInfoRequest]) (*Response, error) { +func (UnimplementedServer) UserInfo(ctx context.Context, r *Request[oidc.UserInfoRequest]) (*Response, error) { return nil, unimplementedError(r) } -func (UnimplementedServer) Revocation(_ context.Context, r *Request[oidc.RevocationRequest]) (*Response, error) { - return nil, unimplementedError(r) +func (UnimplementedServer) Revocation(ctx context.Context, r *ClientRequest[oidc.RevocationRequest]) (*Response, error) { + return nil, unimplementedError(r.Request) } -func (UnimplementedServer) EndSession(_ context.Context, r *Request[oidc.EndSessionRequest]) (*Response, error) { - return nil, unimplementedError(r) -} - -func (UnimplementedServer) Keys(_ context.Context, r *Request[struct{}]) (*Response, error) { +func (UnimplementedServer) EndSession(ctx context.Context, r *Request[oidc.EndSessionRequest]) (*Redirect, error) { return nil, unimplementedError(r) } diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go index 1c810b4..4411619 100644 --- a/pkg/op/server_legacy.go +++ b/pkg/op/server_legacy.go @@ -16,13 +16,11 @@ type LegacyServer struct { readyProbes []ProbesFn } -type none = struct{} - -func (s *LegacyServer) Health(_ context.Context, r *Request[none]) (*Response, error) { +func (s *LegacyServer) Health(_ context.Context, r *Request[struct{}]) (*Response, error) { return NewResponse(Status{Status: "ok"}), nil } -func (s *LegacyServer) Ready(ctx context.Context, r *Request[none]) (*Response, error) { +func (s *LegacyServer) Ready(ctx context.Context, r *Request[struct{}]) (*Response, error) { for _, probe := range s.readyProbes { // shouldn't we run probes in Go routines? if err := probe(ctx); err != nil { @@ -32,12 +30,20 @@ func (s *LegacyServer) Ready(ctx context.Context, r *Request[none]) (*Response, return NewResponse(Status{Status: "ok"}), nil } -func (s *LegacyServer) Discovery(ctx context.Context, r *Request[none]) (*Response, error) { +func (s *LegacyServer) Discovery(ctx context.Context, r *Request[struct{}]) (*Response, error) { return NewResponse( CreateDiscoveryConfig(ctx, s.provider, s.provider.Storage()), ), nil } +func (s *LegacyServer) Keys(ctx context.Context, r *Request[struct{}]) (*Response, error) { + keys, err := s.provider.Storage().KeySet(ctx) + if err != nil { + return nil, NewStatusError(err, http.StatusInternalServerError) + } + return NewResponse(jsonWebKeySet(keys)), nil +} + var ( ErrAuthReqMissingClientID = errors.New("auth request is missing client_id") ErrAuthReqMissingRedirectURI = errors.New("auth request is missing redirect_uri") @@ -264,14 +270,43 @@ func (s *LegacyServer) UserInfo(ctx context.Context, r *Request[oidc.UserInfoReq return NewResponse(info), nil } -func (s *LegacyServer) Revocation(_ context.Context, r *Request[oidc.RevocationRequest]) (*Response, error) { - return nil, unimplementedError(r) +func (s *LegacyServer) Revocation(ctx context.Context, r *ClientRequest[oidc.RevocationRequest]) (*Response, error) { + var subject string + doDecrypt := true + if r.Data.TokenTypeHint != "access_token" { + userID, tokenID, err := s.provider.Storage().GetRefreshTokenInfo(ctx, r.Client.GetID(), r.Data.Token) + if err != nil { + // An invalid refresh token means that we'll try other things (leaving doDecrypt==true) + if !errors.Is(err, ErrInvalidRefreshToken) { + return nil, RevocationError(oidc.ErrServerError().WithParent(err)) + } + } else { + r.Data.Token = tokenID + subject = userID + doDecrypt = false + } + } + if doDecrypt { + tokenID, userID, ok := getTokenIDAndSubjectForRevocation(ctx, s.provider, r.Data.Token) + if ok { + r.Data.Token = tokenID + subject = userID + } + } + if err := s.provider.Storage().RevokeToken(ctx, r.Data.Token, subject, r.Client.GetID()); err != nil { + return nil, RevocationError(err) + } + return NewResponse(nil), nil } -func (s *LegacyServer) EndSession(_ context.Context, r *Request[oidc.EndSessionRequest]) (*Response, error) { - return nil, unimplementedError(r) -} - -func (s *LegacyServer) Keys(_ context.Context, r *Request[struct{}]) (*Response, error) { - return nil, unimplementedError(r) +func (s *LegacyServer) EndSession(ctx context.Context, r *Request[oidc.EndSessionRequest]) (*Redirect, error) { + session, err := ValidateEndSessionRequest(ctx, r.Data, s.provider) + if err != nil { + return nil, err + } + err = s.provider.Storage().TerminateSession(ctx, session.UserID, session.ClientID) + if err != nil { + return nil, err + } + return NewRedirect(session.RedirectURI), nil } diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go index fd1ee93..d19c7f7 100644 --- a/pkg/op/token_revocation.go +++ b/pkg/op/token_revocation.go @@ -131,6 +131,11 @@ func ParseTokenRevocationRequest(r *http.Request, revoker Revoker) (token, token } func RevocationRequestError(w http.ResponseWriter, r *http.Request, err error) { + statusErr := RevocationError(err) + httphelper.MarshalJSONWithStatus(w, statusErr.parent, statusErr.statusCode) +} + +func RevocationError(err error) StatusError { e := oidc.DefaultToServerError(err, err.Error()) status := http.StatusBadRequest switch e.ErrorType { @@ -139,7 +144,7 @@ func RevocationRequestError(w http.ResponseWriter, r *http.Request, err error) { case oidc.ServerError: status = 500 } - httphelper.MarshalJSONWithStatus(w, e, status) + return NewStatusError(e, status) } func getTokenIDAndSubjectForRevocation(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) {