zitadel/internal/api/grpc/oidc/v2/oidc.go

204 lines
7.6 KiB
Go
Raw Normal View History

package oidc
import (
"context"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/op"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
)
func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequestRequest) (*oidc_pb.GetAuthRequestResponse, error) {
authRequest, err := s.query.AuthRequestByID(ctx, true, req.GetAuthRequestId(), true)
if err != nil {
logging.WithError(err).Error("query authRequest by ID")
return nil, err
}
return &oidc_pb.GetAuthRequestResponse{
AuthRequest: authRequestToPb(authRequest),
}, nil
}
func authRequestToPb(a *query.AuthRequest) *oidc_pb.AuthRequest {
pba := &oidc_pb.AuthRequest{
Id: a.ID,
CreationDate: timestamppb.New(a.CreationDate),
ClientId: a.ClientID,
Scope: a.Scope,
RedirectUri: a.RedirectURI,
Prompt: promptsToPb(a.Prompt),
UiLocales: a.UiLocales,
LoginHint: a.LoginHint,
HintUserId: a.HintUserID,
}
if a.MaxAge != nil {
pba.MaxAge = durationpb.New(*a.MaxAge)
}
return pba
}
func promptsToPb(promps []domain.Prompt) []oidc_pb.Prompt {
out := make([]oidc_pb.Prompt, len(promps))
for i, p := range promps {
out[i] = promptToPb(p)
}
return out
}
func promptToPb(p domain.Prompt) oidc_pb.Prompt {
switch p {
case domain.PromptUnspecified:
return oidc_pb.Prompt_PROMPT_UNSPECIFIED
case domain.PromptNone:
return oidc_pb.Prompt_PROMPT_NONE
case domain.PromptLogin:
return oidc_pb.Prompt_PROMPT_LOGIN
case domain.PromptConsent:
return oidc_pb.Prompt_PROMPT_CONSENT
case domain.PromptSelectAccount:
return oidc_pb.Prompt_PROMPT_SELECT_ACCOUNT
case domain.PromptCreate:
return oidc_pb.Prompt_PROMPT_CREATE
default:
return oidc_pb.Prompt_PROMPT_UNSPECIFIED
}
}
func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) {
switch v := req.GetCallbackKind().(type) {
case *oidc_pb.CreateCallbackRequest_Error:
return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error)
case *oidc_pb.CreateCallbackRequest_Session:
return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session)
default:
return nil, zerrors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v)
}
}
func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *oidc_pb.AuthorizationError) (*oidc_pb.CreateCallbackResponse, error) {
details, aar, err := s.command.FailAuthRequest(ctx, authRequestID, errorReasonToDomain(ae.GetError()))
if err != nil {
return nil, err
}
authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar}
callback, err := oidc.CreateErrorCallbackURL(authReq, errorReasonToOIDC(ae.GetError()), ae.GetErrorDescription(), ae.GetErrorUri(), s.op.Provider())
if err != nil {
return nil, err
}
return &oidc_pb.CreateCallbackResponse{
Details: object.DomainToDetailsPb(details),
CallbackUrl: callback,
}, nil
}
func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID string, session *oidc_pb.Session) (*oidc_pb.CreateCallbackResponse, error) {
details, aar, err := s.command.LinkSessionToAuthRequest(ctx, authRequestID, session.GetSessionId(), session.GetSessionToken(), true)
if err != nil {
return nil, err
}
authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar}
feat: trusted (instance) domains (#8369) # Which Problems Are Solved ZITADEL currently selects the instance context based on a HTTP header (see https://github.com/zitadel/zitadel/issues/8279#issue-2399959845 and checks it against the list of instance domains. Let's call it instance or API domain. For any context based URL (e.g. OAuth, OIDC, SAML endpoints, links in emails, ...) the requested domain (instance domain) will be used. Let's call it the public domain. In cases of proxied setups, all exposed domains (public domains) require the domain to be managed as instance domain. This can either be done using the "ExternalDomain" in the runtime config or via system API, which requires a validation through CustomerPortal on zitadel.cloud. # How the Problems Are Solved - Two new headers / header list are added: - `InstanceHostHeaders`: an ordered list (first sent wins), which will be used to match the instance. (For backward compatibility: the `HTTP1HostHeader`, `HTTP2HostHeader` and `forwarded`, `x-forwarded-for`, `x-forwarded-host` are checked afterwards as well) - `PublicHostHeaders`: an ordered list (first sent wins), which will be used as public host / domain. This will be checked against a list of trusted domains on the instance. - The middleware intercepts all requests to the API and passes a `DomainCtx` object with the hosts and protocol into the context (previously only a computed `origin` was passed) - HTTP / GRPC server do not longer try to match the headers to instances themself, but use the passed `http.DomainContext` in their interceptors. - The `RequestedHost` and `RequestedDomain` from authz.Instance are removed in favor of the `http.DomainContext` - When authenticating to or signing out from Console UI, the current `http.DomainContext(ctx).Origin` (already checked by instance interceptor for validity) is used to compute and dynamically add a `redirect_uri` and `post_logout_redirect_uri`. - Gateway passes all configured host headers (previously only did `x-zitadel-*`) - Admin API allows to manage trusted domain # Additional Changes None # Additional Context - part of #8279 - open topics: - "single-instance" mode - Console UI
2024-07-31 15:00:38 +00:00
ctx = op.ContextWithIssuer(ctx, http.DomainContext(ctx).Origin())
var callback string
if aar.ResponseType == domain.OIDCResponseTypeCode {
callback, err = oidc.CreateCodeCallbackURL(ctx, authReq, s.op.Provider())
} else {
perf(oidc): optimize token creation (#7822) * implement code exchange * port tokenexchange to v2 tokens * implement refresh token * implement client credentials * implement jwt profile * implement device token * cleanup unused code * fix current unit tests * add user agent unit test * unit test domain package * need refresh token as argument * test commands create oidc session * test commands device auth * fix device auth build error * implicit for oidc session API * implement authorize callback handler for legacy implicit mode * upgrade oidc module to working draft * add missing auth methods and time * handle all errors in defer * do not fail auth request on error the oauth2 Go client automagically retries on any error. If we fail the auth request on the first error, the next attempt will always fail with the Errors.AuthRequest.NoCode, because the auth request state is already set to failed. The original error is then already lost and the oauth2 library does not return the original error. Therefore we should not fail the auth request. Might be worth discussing and perhaps send a bug report to Oauth2? * fix code flow tests by explicitly setting code exchanged * fix unit tests in command package * return allowed scope from client credential client * add device auth done reducer * carry nonce thru session into ID token * fix token exchange integration tests * allow project role scope prefix in client credentials client * gci formatting * do not return refresh token in client credentials and jwt profile * check org scope * solve linting issue on authorize callback error * end session based on v2 session ID * use preferred language and user agent ID for v2 access tokens * pin oidc v3.23.2 * add integration test for jwt profile and client credentials with org scopes * refresh token v1 to v2 * add user token v2 audit event * add activity trigger * cleanup and set panics for unused methods * use the encrypted code for v1 auth request get by code * add missing event translation * fix pipeline errors (hopefully) * fix another test * revert pointer usage of preferred language * solve browser info panic in device auth * remove duplicate entries in AMRToAuthMethodTypes to prevent future `mfa` claim * revoke v1 refresh token to prevent reuse * fix terminate oidc session * always return a new refresh toke in refresh token grant --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
2024-05-16 05:07:56 +00:00
callback, err = s.op.CreateTokenCallbackURL(ctx, authReq)
}
if err != nil {
return nil, err
}
return &oidc_pb.CreateCallbackResponse{
Details: object.DomainToDetailsPb(details),
CallbackUrl: callback,
}, nil
}
func errorReasonToDomain(errorReason oidc_pb.ErrorReason) domain.OIDCErrorReason {
switch errorReason {
case oidc_pb.ErrorReason_ERROR_REASON_UNSPECIFIED:
return domain.OIDCErrorReasonUnspecified
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST:
return domain.OIDCErrorReasonInvalidRequest
case oidc_pb.ErrorReason_ERROR_REASON_UNAUTHORIZED_CLIENT:
return domain.OIDCErrorReasonUnauthorizedClient
case oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED:
return domain.OIDCErrorReasonAccessDenied
case oidc_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE:
return domain.OIDCErrorReasonUnsupportedResponseType
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_SCOPE:
return domain.OIDCErrorReasonInvalidScope
case oidc_pb.ErrorReason_ERROR_REASON_SERVER_ERROR:
return domain.OIDCErrorReasonServerError
case oidc_pb.ErrorReason_ERROR_REASON_TEMPORARY_UNAVAILABLE:
return domain.OIDCErrorReasonTemporaryUnavailable
case oidc_pb.ErrorReason_ERROR_REASON_INTERACTION_REQUIRED:
return domain.OIDCErrorReasonInteractionRequired
case oidc_pb.ErrorReason_ERROR_REASON_LOGIN_REQUIRED:
return domain.OIDCErrorReasonLoginRequired
case oidc_pb.ErrorReason_ERROR_REASON_ACCOUNT_SELECTION_REQUIRED:
return domain.OIDCErrorReasonAccountSelectionRequired
case oidc_pb.ErrorReason_ERROR_REASON_CONSENT_REQUIRED:
return domain.OIDCErrorReasonConsentRequired
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_URI:
return domain.OIDCErrorReasonInvalidRequestURI
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_OBJECT:
return domain.OIDCErrorReasonInvalidRequestObject
case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_NOT_SUPPORTED:
return domain.OIDCErrorReasonRequestNotSupported
case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_URI_NOT_SUPPORTED:
return domain.OIDCErrorReasonRequestURINotSupported
case oidc_pb.ErrorReason_ERROR_REASON_REGISTRATION_NOT_SUPPORTED:
return domain.OIDCErrorReasonRegistrationNotSupported
default:
return domain.OIDCErrorReasonUnspecified
}
}
func errorReasonToOIDC(reason oidc_pb.ErrorReason) string {
switch reason {
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST:
return "invalid_request"
case oidc_pb.ErrorReason_ERROR_REASON_UNAUTHORIZED_CLIENT:
return "unauthorized_client"
case oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED:
return "access_denied"
case oidc_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE:
return "unsupported_response_type"
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_SCOPE:
return "invalid_scope"
case oidc_pb.ErrorReason_ERROR_REASON_TEMPORARY_UNAVAILABLE:
return "temporarily_unavailable"
case oidc_pb.ErrorReason_ERROR_REASON_INTERACTION_REQUIRED:
return "interaction_required"
case oidc_pb.ErrorReason_ERROR_REASON_LOGIN_REQUIRED:
return "login_required"
case oidc_pb.ErrorReason_ERROR_REASON_ACCOUNT_SELECTION_REQUIRED:
return "account_selection_required"
case oidc_pb.ErrorReason_ERROR_REASON_CONSENT_REQUIRED:
return "consent_required"
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_URI:
return "invalid_request_uri"
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_OBJECT:
return "invalid_request_object"
case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_NOT_SUPPORTED:
return "request_not_supported"
case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_URI_NOT_SUPPORTED:
return "request_uri_not_supported"
case oidc_pb.ErrorReason_ERROR_REASON_REGISTRATION_NOT_SUPPORTED:
return "registration_not_supported"
case oidc_pb.ErrorReason_ERROR_REASON_UNSPECIFIED, oidc_pb.ErrorReason_ERROR_REASON_SERVER_ERROR:
fallthrough
default:
return "server_error"
}
}