diff --git a/.dockerignore b/.dockerignore index 4d913fbbc91..649aa99fe29 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,3 +14,5 @@ dist test/e2e test/mock-* cypress +docker-compose +Dockerfile diff --git a/consent/handler.go b/consent/handler.go index 78a5897d5da..ecbc50e57ed 100644 --- a/consent/handler.go +++ b/consent/handler.go @@ -48,6 +48,7 @@ const ( LoginPath = "/oauth2/auth/requests/login" ConsentPath = "/oauth2/auth/requests/consent" LogoutPath = "/oauth2/auth/requests/logout" + DevicePath = "/oauth2/auth/requests/device/usercode" SessionsPath = "/oauth2/auth/sessions" ) @@ -77,6 +78,7 @@ func (h *Handler) SetRoutes(admin *x.RouterAdmin) { admin.GET(LogoutPath, h.GetLogoutRequest) admin.PUT(LogoutPath+"/accept", h.AcceptLogoutRequest) admin.PUT(LogoutPath+"/reject", h.RejectLogoutRequest) + admin.POST(DevicePath+"/verify", h.VerifyDeviceAuthUserCode) } // swagger:route DELETE /oauth2/auth/sessions/consent admin revokeConsentSessions @@ -782,3 +784,88 @@ func (h *Handler) GetLogoutRequest(w http.ResponseWriter, r *http.Request, ps ht h.r.Writer().Write(w, r, request) } + +func (h *Handler) VerifyDeviceAuthUserCode(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + type payload struct { + UserCode string `json:"userCode"` + } + + var body payload + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) + return + } + + if body.UserCode == "" { + h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Request body parameter 'userCode' is not defined but should have been.`))) + return + } + + // Find the User Code in the DB if it exists + userCodeSession, err := h.r.OAuth2Storage().GetUserCodeSession(r.Context(), body.UserCode, nil) + if err != nil { + if errors.Is(err, fosite.ErrNotFound) { + h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) + } + h.r.Writer().WriteError(w, r, err) + return + } + + // Check expiry of User Code + // ... + + // Check that it hasn't already been used + // ... + + // Find the Device Link Request using the Request ID of the User Code Session + deviceLinkReq, err := h.r.ConsentManager().GetDeviceLinkRequest(r.Context(), userCodeSession.GetID()) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + // Invalidate the User Code to ensure it can't be used again. + // if err := h.r.OAuth2Storage().InvalidateUserCodeSession(r.Context(), body.UserCode); err != nil { + // h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) + // return + // } + + // Should we create a HandledDeviceLinkRequest? + + // p.ID = challenge + // ar, err := h.r.ConsentManager().GetLoginRequest(r.Context(), challenge) + // if err != nil { + // h.r.Writer().WriteError(w, r, err) + // return + // } else if ar.Subject != "" && p.Subject != ar.Subject { + // h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Field 'subject' does not match subject from previous authentication."))) + // return + // } + + // if ar.Skip { + // p.Remember = true // If skip is true remember is also true to allow consecutive calls as the same user! + // p.AuthenticatedAt = ar.AuthenticatedAt + // } else { + // p.AuthenticatedAt = sqlxx.NullTime(time.Now().UTC(). + // // Rounding is important to avoid SQL time synchronization issues in e.g. MySQL! + // Truncate(time.Second)) + // ar.AuthenticatedAt = p.AuthenticatedAt + // } + // p.RequestedAt = ar.RequestedAt + + // request, err := h.r.ConsentManager().HandleLoginRequest(r.Context(), challenge, &p) + // if err != nil { + // h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) + // return + // } + + ru, err := url.Parse(deviceLinkReq.RequestURL) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + h.r.Writer().Write(w, r, &RequestHandlerResponse{ + RedirectTo: urlx.SetQuery(ru, url.Values{"link_verifier": {deviceLinkReq.Verifier}}).String(), + }) +} diff --git a/consent/helper.go b/consent/helper.go index ef76f7921f9..200ec55c811 100644 --- a/consent/helper.go +++ b/consent/helper.go @@ -33,7 +33,7 @@ import ( "github.com/ory/hydra/client" ) -func sanitizeClientFromRequest(ar fosite.AuthorizeRequester) *client.Client { +func sanitizeClientFromRequest(ar fosite.Requester) *client.Client { return sanitizeClient(ar.GetClient().(*client.Client)) } diff --git a/consent/manager.go b/consent/manager.go index f0fa286050b..b5453763a1f 100644 --- a/consent/manager.go +++ b/consent/manager.go @@ -64,6 +64,15 @@ type Manager interface { CreateForcedObfuscatedLoginSession(ctx context.Context, session *ForcedObfuscatedLoginSession) error GetForcedObfuscatedLoginSession(ctx context.Context, client, obfuscated string) (*ForcedObfuscatedLoginSession, error) + // Functions for the management of DeviceLink requests + CreateDeviceLinkRequest(ctx context.Context, req *DeviceLinkRequest) error + GetDeviceLinkRequest(ctx context.Context, challenge string) (*DeviceLinkRequest, error) + GetDeviceLinkRequestByVerifier(ctx context.Context, verifier string) (*DeviceLinkRequest, error) + // GetDeviceLinkRequestByUserCode(ctx context.Context, userCode string) (*DeviceLinkRequest, error) + // GetDeviceLinkRequestByDeviceCode(ctx context.Context, deviceCode string) (*DeviceLinkRequest, error) + // VerifyAndInvalidateDeviceLinkRequest(ctx context.Context, verifier string) (*HandledDeviceLinkRequest, error) + + ListUserAuthenticatedClientsWithFrontChannelLogout(ctx context.Context, subject, sid string) ([]client.Client, error) ListUserAuthenticatedClientsWithBackChannelLogout(ctx context.Context, subject, sid string) ([]client.Client, error) diff --git a/consent/strategy.go b/consent/strategy.go index fa2f9eebfed..6b942f1cb43 100644 --- a/consent/strategy.go +++ b/consent/strategy.go @@ -30,5 +30,6 @@ var _ Strategy = new(DefaultStrategy) type Strategy interface { HandleOAuth2AuthorizationRequest(w http.ResponseWriter, r *http.Request, req fosite.AuthorizeRequester) (*HandledConsentRequest, error) + HandleOAuth2DeviceAuthorizationRequest(w http.ResponseWriter, r *http.Request, req fosite.DeviceAuthorizeRequester) (*HandledConsentRequest, error) HandleOpenIDConnectLogout(w http.ResponseWriter, r *http.Request) (*LogoutResult, error) } diff --git a/consent/strategy_default.go b/consent/strategy_default.go index ad4e8b08076..bf9c9f62562 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -207,7 +207,7 @@ func (s *DefaultStrategy) getSubjectFromIDTokenHint(ctx context.Context, idToken return sub, nil } -func (s *DefaultStrategy) forwardAuthenticationRequest(w http.ResponseWriter, r *http.Request, ar fosite.AuthorizeRequester, subject string, authenticatedAt time.Time, session *LoginSession) error { +func (s *DefaultStrategy) forwardAuthenticationRequest(w http.ResponseWriter, r *http.Request, ar fosite.Requester, subject string, authenticatedAt time.Time, session *LoginSession) error { if (subject != "" && authenticatedAt.IsZero()) || (subject == "" && !authenticatedAt.IsZero()) { return errorsx.WithStack(fosite.ErrServerError.WithHint("Consent strategy returned a non-empty subject with an empty auth date, or an empty subject with a non-empty auth date.")) } @@ -230,6 +230,14 @@ func (s *DefaultStrategy) forwardAuthenticationRequest(w http.ResponseWriter, r // Generate the request URL iu := s.c.OAuth2AuthURL() + + // Identify requester type + if _, ok := ar.(fosite.AuthorizeRequester); ok { + iu = s.c.OAuth2AuthURL() + } + if _, ok := ar.(*fosite.DeviceAuthorizeRequest); ok { + iu = s.c.OAuth2DeviceAuthURL() + } iu.RawQuery = r.URL.RawQuery var idTokenHintClaims jwtgo.MapClaims @@ -527,7 +535,7 @@ func (s *DefaultStrategy) requestConsent(w http.ResponseWriter, r *http.Request, return s.forwardConsentRequest(w, r, ar, authenticationSession, nil) } -func (s *DefaultStrategy) forwardConsentRequest(w http.ResponseWriter, r *http.Request, ar fosite.AuthorizeRequester, as *HandledLoginRequest, cs *HandledConsentRequest) error { +func (s *DefaultStrategy) forwardConsentRequest(w http.ResponseWriter, r *http.Request, ar fosite.Requester, as *HandledLoginRequest, cs *HandledConsentRequest) error { skip := false if cs != nil { skip = true @@ -583,7 +591,7 @@ func (s *DefaultStrategy) forwardConsentRequest(w http.ResponseWriter, r *http.R return errorsx.WithStack(ErrAbortOAuth2Request) } -func (s *DefaultStrategy) verifyConsent(w http.ResponseWriter, r *http.Request, req fosite.AuthorizeRequester, verifier string) (*HandledConsentRequest, error) { +func (s *DefaultStrategy) verifyConsent(w http.ResponseWriter, r *http.Request, req fosite.Requester, verifier string) (*HandledConsentRequest, error) { session, err := s.r.ConsentManager().VerifyAndInvalidateConsentRequest(r.Context(), verifier) if errors.Is(err, sqlcon.ErrNoRows) { return nil, errorsx.WithStack(fosite.ErrAccessDenied.WithHint("The consent verifier has already been used, has not been granted, or is invalid.")) @@ -1015,3 +1023,204 @@ func (s *DefaultStrategy) HandleOAuth2AuthorizationRequest(w http.ResponseWriter return consentSession, nil } + +func (s *DefaultStrategy) HandleOAuth2DeviceAuthorizationRequest(w http.ResponseWriter, r *http.Request, req fosite.DeviceAuthorizeRequester) (*HandledConsentRequest, error) { + + // 1 - Get the link_verifier value from the request + linkVerifier := strings.TrimSpace(req.GetRequestForm().Get("link_verifier")) + authenticationVerifier := strings.TrimSpace(req.GetRequestForm().Get("login_verifier")) + consentVerifier := strings.TrimSpace(req.GetRequestForm().Get("consent_verifier")) + + // Final stage of the device auth flow + if len(consentVerifier) > 0 { + handledConsent, err := s.verifyConsent(w, r, req, consentVerifier) + if err != nil { + return nil, err + } + + return handledConsent, nil + } + + // Authentication has been accepted + if len(authenticationVerifier) > 0 { + authSession, err := s.verifyDeviceGrantAuthentication(w, r, req, authenticationVerifier) + if err != nil { + return nil, err + } + + // Update Link Request with reference to Login Challenge + + // ok, we need to process this request and redirect to auth endpoint + return nil, s.forwardConsentRequest(w, r, req, authSession, nil) + } + + // User Code linking has been accepted + if len(linkVerifier) > 0 { + // handledLinkRequest, err := s.verifyUserCodeLink(w, r, req, linkVerifier) + // if err != nil { + // return nil, err + // } + + return nil, s.forwardAuthenticationRequest(w, r, req, "", time.Time{}, nil) + // return nil, s.requestAuthentication(w, r, req) + } + + // Generate the request URL + // reqURL := s.c.OAuth2DeviceAuthURL() + // reqURL.RawQuery = r.URL.RawQuery + + // linkRequest := &DeviceLinkRequest{ + // ID: req.GetID(), + // Verifier: strings.Replace(uuid.New(), "-", "", -1), + // RequestedScope: []string(req.GetRequestedScopes()), + // RequestedAudience: []string(req.GetRequestedAudience()), + // Client: sanitizeClient(req.GetClient().(*client.Client)), + // RequestedAt: time.Now().Truncate(time.Second).UTC(), + // OpenIDConnectContext: nil, + // RequestURL: reqURL.String(), + // } + + // // Persist the Device Link Request + // if err := s.r.ConsentManager().CreateDeviceLinkRequest(r.Context(), linkRequest); err != nil { + // return nil, errorsx.WithStack(err) + // } + + return nil, nil +} + +func (s *DefaultStrategy) verifyDeviceGrantAuthentication(w http.ResponseWriter, r *http.Request, req fosite.DeviceAuthorizeRequester, verifier string) (*HandledLoginRequest, error) { + ctx := r.Context() + session, err := s.r.ConsentManager().VerifyAndInvalidateLoginRequest(ctx, verifier) + if errors.Is(err, sqlcon.ErrNoRows) { + return nil, errorsx.WithStack(fosite.ErrAccessDenied.WithHint("The login verifier has already been used, has not been granted, or is invalid.")) + } else if err != nil { + return nil, err + } + + if session.HasError() { + session.Error.SetDefaults(loginRequestDeniedErrorName) + return nil, errorsx.WithStack(session.Error.toRFCError()) + } + + if session.RequestedAt.Add(s.c.ConsentRequestMaxAge()).Before(time.Now()) { + return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.WithHint("The login request has expired. Please try again.")) + } + + if err := validateCsrfSession(r, s.r.CookieStore(), cookieAuthenticationCSRFName, session.LoginRequest.CSRF, s.c.CookieSameSiteLegacyWorkaround(), s.c.TLS(config.PublicInterface).Enabled()); err != nil { + return nil, err + } + + if session.LoginRequest.Skip && !session.Remember { + return nil, errorsx.WithStack(fosite.ErrServerError.WithHint("The login request was previously remembered and can only be forgotten using the reject feature.")) + } + + if session.LoginRequest.Skip && session.Subject != session.LoginRequest.Subject { + // Revoke the session because there's clearly a mix up wrt the subject that's being authenticated + if err := s.revokeAuthenticationSession(w, r); err != nil { + return nil, err + } + + return nil, errorsx.WithStack(fosite.ErrServerError.WithHint("The login request is marked as remember, but the subject from the login confirmation does not match the original subject from the cookie.")) + } + + subjectIdentifier, err := s.obfuscateSubjectIdentifier(req.GetClient(), session.Subject, session.ForceSubjectIdentifier) + if err != nil { + return nil, err + } + + sessionID := session.LoginRequest.SessionID.String() + + if err := s.r.OpenIDConnectRequestValidator().ValidatePrompt(ctx, &fosite.AuthorizeRequest{ + // ResponseTypes: req.GetResponseTypes(), + // RedirectURI: req.GetRedirectURI(), + // State: req.GetState(), + // HandledResponseTypes, this can be safely ignored because it's not being used by validation + Request: fosite.Request{ + ID: req.GetID(), + RequestedAt: req.GetRequestedAt(), + Client: req.GetClient(), + RequestedAudience: req.GetRequestedAudience(), + GrantedAudience: req.GetGrantedAudience(), + RequestedScope: req.GetRequestedScopes(), + GrantedScope: req.GetGrantedScopes(), + Form: req.GetRequestForm(), + Session: &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: subjectIdentifier, + IssuedAt: time.Now().UTC(), // doesn't matter + ExpiresAt: time.Now().Add(time.Hour).UTC(), // doesn't matter + AuthTime: time.Time(session.AuthenticatedAt), + RequestedAt: session.RequestedAt, + }, + Headers: &jwt.Headers{}, + Subject: session.Subject, + }, + }, + }); errors.Is(err, fosite.ErrLoginRequired) { + // This indicates that something went wrong with checking the subject id - let's destroy the session to be safe + if err := s.revokeAuthenticationSession(w, r); err != nil { + return nil, err + } + + return nil, err + } else if err != nil { + return nil, err + } + + if session.ForceSubjectIdentifier != "" { + if err := s.r.ConsentManager().CreateForcedObfuscatedLoginSession(r.Context(), &ForcedObfuscatedLoginSession{ + Subject: session.Subject, + ClientID: req.GetClient().GetID(), + SubjectObfuscated: session.ForceSubjectIdentifier, + }); err != nil { + return nil, err + } + } + + if !session.LoginRequest.Skip { + if time.Time(session.AuthenticatedAt).IsZero() { + return nil, errorsx.WithStack(fosite.ErrServerError.WithHint("Expected the handled login request to contain a valid authenticated_at value but it was zero. This is a bug which should be reported to https://github.com/ory/hydra.")) + } + + if err := s.r.ConsentManager().ConfirmLoginSession(r.Context(), sessionID, time.Time(session.AuthenticatedAt), session.Subject, session.Remember); err != nil { + return nil, err + } + } + + if !session.Remember && !session.LoginRequest.Skip { + // If the session should not be remembered (and we're actually not skipping), than the user clearly don't + // wants us to store a cookie. So let's bust the authentication session (if one exists). + if err := s.revokeAuthenticationSession(w, r); err != nil { + return nil, err + } + } + + if !session.Remember || session.LoginRequest.Skip { + // If the user doesn't want to remember the session, we do not store a cookie. + // If login was skipped, it means an authentication cookie was present and + // we don't want to touch it (in order to preserve its original expiry date) + return session, nil + } + + // Not a skipped login and the user asked to remember its session, store a cookie + cookie, _ := s.r.CookieStore().Get(r, CookieName(s.c.TLS(config.PublicInterface).Enabled(), CookieAuthenticationName)) + cookie.Values[CookieAuthenticationSIDName] = sessionID + if session.RememberFor >= 0 { + cookie.Options.MaxAge = session.RememberFor + } + cookie.Options.HttpOnly = true + cookie.Options.SameSite = s.c.CookieSameSiteMode() + cookie.Options.Secure = s.c.TLS(config.PublicInterface).Enabled() + if err := cookie.Save(r, w); err != nil { + return nil, errorsx.WithStack(err) + } + + s.r.Logger().WithRequest(r). + WithFields(logrus.Fields{ + "cookie_name": CookieName(s.c.TLS(config.PublicInterface).Enabled(), CookieAuthenticationName), + "cookie_http_only": true, + "cookie_same_site": s.c.CookieSameSiteMode(), + "cookie_secure": s.c.TLS(config.PublicInterface).Enabled(), + }).Debug("Authentication session cookie was set.") + return session, nil +} \ No newline at end of file diff --git a/consent/types.go b/consent/types.go index 332b3db0466..1a3c63a9f4e 100644 --- a/consent/types.go +++ b/consent/types.go @@ -704,3 +704,81 @@ func NewConsentRequestSessionData() *ConsentRequestSessionData { IDToken: map[string]interface{}{}, } } + + +// Contains information on an ongoing device link request. +// +// swagger:model deviceLinkRequest +type DeviceLinkRequest struct { + // ID is the identifier ("link challenge") of the device link request. It is used to + // identify the session. + // + // required: true + ID string `json:"challenge" db:"challenge"` + + DeviceCode string `json:"device_code" db:"device_code"` + + // RequestedScope contains the OAuth 2.0 Scope requested by the OAuth 2.0 Client. + // + // required: true + RequestedScope sqlxx.StringSlicePipeDelimiter `json:"requested_scope" db:"requested_scope"` + + // RequestedScope contains the access token audience as requested by the OAuth 2.0 Client. + // + // required: true + RequestedAudience sqlxx.StringSlicePipeDelimiter `json:"requested_access_token_audience" db:"requested_at_audience"` + + // OpenIDConnectContext provides context for the (potential) OpenID Connect context. Implementation of these + // values in your app are optional but can be useful if you want to be fully compliant with the OpenID Connect spec. + OpenIDConnectContext *OpenIDConnectContext `json:"oidc_context" db:"oidc_context"` + + // Client is the OAuth 2.0 Client that initiated the request. + // + // required: true + Client *client.Client `json:"client" db:"-"` + + ClientID string `json:"-" db:"client_id"` + + // RequestURL is the original OAuth 2.0 Device Authorization URL requested by the OAuth 2.0 client. It is the URL which + // initiates the OAuth 2.0 Device Authorization Grant. + // + // required: true + RequestURL string `json:"request_url" db:"request_url"` + + // LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate + // a login and consent request in the login & consent app. + LoginChallenge sqlxx.NullString `json:"login_challenge" db:"login_challenge"` + + // If set to true means that the request was already handled. This + // can happen on form double-submit or other errors. If this is set + // we recommend redirecting the user to `request_url` to re-initiate + // the flow. + WasHandled bool `json:"-" db:"was_handled,r"` + + Verifier string `json:"-" db:"verifier"` + + // AuthenticatedAt sqlxx.NullTime `json:"-" db:"authenticated_at"` + RequestedAt time.Time `json:"-" db:"requested_at"` +} + +func (DeviceLinkRequest) TableName() string { + return "hydra_oauth2_device_link_request" +} + +func (r *DeviceLinkRequest) FindInDB(c *pop.Connection, id string) error { + return c.Select("hydra_oauth2_device_link_request.*"). + // LeftJoin("hydra_oauth2_device_link_request_handled as hr", "hydra_oauth2_device_link_request.challenge = hr.challenge"). + Find(r, id) +} + +func (r *DeviceLinkRequest) BeforeSave(_ *pop.Connection) error { + if r.Client != nil { + r.ClientID = r.Client.OutfacingID + } + return nil +} + +func (r *DeviceLinkRequest) AfterFind(c *pop.Connection) error { + r.Client = &client.Client{} + return sqlcon.HandleError(c.Where("id = ?", r.ClientID).First(r.Client)) +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000000..d3df163dd20 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,60 @@ +version: "3.7" +services: + hydra-build-go: + container_name: hydra-build-go + depends_on: + - init-dev + image: oryd/hydra:latest-sqlite + command: serve all --dangerous-force-http + # command: serve -c /etc/config/hydra/hydra.yml all --dangerous-force-http + environment: + - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true + - SECRETS_SYSTEM=$SECRETS_SYSTEM + volumes: + - type: volume + source: hydra-sqlite + target: /var/lib/sqlite + # - type: bind + # source: ./config_hydra.yml + # target: /etc/config/hydra/hydra.yml + restart: unless-stopped + networks: + intranet: + oathkeeper: + # entrypoint: /bin/sh -c "while sleep 1000; do :; done;" + hydra-migrate: + image: oryd/hydra:latest-sqlite + environment: + - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true + command: migrate sql -e --yes + # command: migrate -c /etc/config/hydra/hydra.yml sql -e --yes + volumes: + - type: volume + source: hydra-sqlite + target: /var/lib/sqlite + read_only: false + # - type: bind + # source: ./config_hydra.yml + # target: /etc/config/hydra/hydra.yml + restart: on-failure + networks: + intranet: + init-dev: + image: go-build + container_name: go-builder + build: . + entrypoint: /bin/bash -c "while sleep 1000; do :; done;" + volumes: + - type: bind + source: /var/run/docker.sock + target: /var/run/docker.sock + - type: bind + source: ${PWD} + target: $PWD + +networks: + intranet: + oathkeeper: + external: true +volumes: + hydra-sqlite: \ No newline at end of file diff --git a/driver/config/provider.go b/driver/config/provider.go index d96c0cb54c0..d2c3cba63eb 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -35,6 +35,7 @@ const ( KeyOAuth2ClientRegistrationURL = "webfinger.oidc_discovery.client_registration_url" KeyOAuth2TokenURL = "webfinger.oidc_discovery.token_url" // #nosec G101 KeyOAuth2AuthURL = "webfinger.oidc_discovery.auth_url" + KeyOAuth2DeviceAuthURL = "webfinger.oidc_discovery.device_auth_url" KeyJWKSURL = "webfinger.oidc_discovery.jwks_url" KeyOIDCDiscoverySupportedClaims = "webfinger.oidc_discovery.supported_claims" KeyOIDCDiscoverySupportedScope = "webfinger.oidc_discovery.supported_scope" @@ -51,6 +52,8 @@ const ( KeyRefreshTokenLifespan = "ttl.refresh_token" // #nosec G101 KeyIDTokenLifespan = "ttl.id_token" // #nosec G101 KeyAuthCodeLifespan = "ttl.auth_code" + KeyDeviceCodeLifespan = "ttl.device_code" + KeyUserCodeLifespan = "ttl.user_code" KeyScopeStrategy = "strategies.scope" KeyGetCookieSecrets = "secrets.cookie" KeyGetSystemSecret = "secrets.system" @@ -58,12 +61,14 @@ const ( KeyLoginURL = "urls.login" KeyLogoutURL = "urls.logout" KeyConsentURL = "urls.consent" + KeyDeviceLinkURL = "urls.device_link" KeyErrorURL = "urls.error" KeyPublicURL = "urls.self.public" KeyIssuerURL = "urls.self.issuer" KeyAccessTokenStrategy = "strategies.access_token" KeySubjectIdentifierAlgorithmSalt = "oidc.subject_identifiers.pairwise.salt" KeyPublicAllowDynamicRegistration = "oidc.dynamic_client_registration.enabled" + KeyDeviceAuthTokenPollingInterval = "oauth2.device_authorization.token_polling_interval" KeyPKCEEnforced = "oauth2.pkce.enforced" KeyPKCEEnforcedForPublicClients = "oauth2.pkce.enforced_for_public_clients" KeyLogLevel = "log.level" @@ -261,6 +266,23 @@ func (p *Provider) AuthCodeLifespan() time.Duration { return p.p.DurationF(KeyAuthCodeLifespan, time.Minute*10) } +func (p *Provider) DeviceCodeLifespan() time.Duration { + return p.p.DurationF(KeyDeviceCodeLifespan, time.Minute*15) +} + +func (p *Provider) UserCodeLifespan() time.Duration { + return p.p.DurationF(KeyUserCodeLifespan, time.Minute*15) +} + +func (p *Provider) DeviceAuthTokenPollingInterval() time.Duration { + return p.p.DurationF(KeyDeviceAuthTokenPollingInterval, time.Second*5) +} + +func (p *Provider) DeviceAuthVerificationURI() string { + return p.p.String(KeyDeviceLinkURL) +} + + func (p *Provider) ScopeStrategy() string { return p.p.String(KeyScopeStrategy) } @@ -391,6 +413,11 @@ func (p *Provider) JWKSURL() *url.URL { return p.p.RequestURIF(KeyJWKSURL, urlx.AppendPaths(p.IssuerURL(), "/.well-known/jwks.json")) } +func (p *Provider) OAuth2DeviceAuthURL() *url.URL { + return p.p.RequestURIF(KeyOAuth2DeviceAuthURL, urlx.AppendPaths(p.PublicURL(), "/oauth2/device/auth")) +} + + func (p *Provider) TokenRefreshHookURL() *url.URL { return p.p.URIF(KeyRefreshTokenHookURL, nil) } diff --git a/driver/registry_base.go b/driver/registry_base.go index 56dc44f8c2f..77e09a7bfa0 100644 --- a/driver/registry_base.go +++ b/driver/registry_base.go @@ -288,6 +288,12 @@ func (m *RegistryBase) oAuth2Config() *compose.Config { AccessTokenLifespan: m.C.AccessTokenLifespan(), RefreshTokenLifespan: m.C.RefreshTokenLifespan(), AuthorizeCodeLifespan: m.C.AuthCodeLifespan(), + DeviceAuthorisation: compose.DeviceAuthorisationConfig{ + DeviceCodeLifespan: m.C.DeviceCodeLifespan(), + UserCodeLifespan: m.C.UserCodeLifespan(), + TokenPollingInterval: m.C.DeviceAuthTokenPollingInterval(), + VerificationURI: m.C.DeviceAuthVerificationURI(), + }, IDTokenLifespan: m.C.IDTokenLifespan(), IDTokenIssuer: m.C.IssuerURL().String(), HashCost: m.C.BCryptCost(), @@ -348,6 +354,7 @@ func (m *RegistryBase) OAuth2Provider() fosite.OAuth2Provider { compose.OAuth2AuthorizeExplicitFactory, compose.OAuth2AuthorizeImplicitFactory, compose.OAuth2ClientCredentialsGrantFactory, + compose.OAuth2DeviceAuthorizeFactory, compose.OAuth2RefreshTokenGrantFactory, compose.OpenIDConnectExplicitFactory, compose.OpenIDConnectHybridFactory, diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000000..5c0f50f23ba --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +FILE_CHANGE_LOG_FILE=/tmp/changes.log +SERVICE_ARGS="$@" + +echo "*******$SERVICE_ARGS*********" + +log() { + echo "***** $1 *****" +} + +init() { + log "Initializing" + truncate -s 0 ${FILE_CHANGE_LOG_FILE} + tail -f ${FILE_CHANGE_LOG_FILE} & +} + +build() { + log "Building ${SERVICE_NAME} binary" + go env -w GOPROXY="proxy.golang.org,direct" + go mod download + go build -gcflags "all=-N -l" -o /${SERVICE_NAME} +} + +start() { + log "Starting Delve" + # ./entrypoint.sh serve all -c ../hydra/config.yml --dangerous-force-http + # dlv --listen=:20001 --headless=true --api-version=2 --accept-multiclient exec /${SERVICE_NAME} -- ${SERVICE_ARGS} & + /${SERVICE_NAME} ${SERVICE_ARGS} & +} + +restart() { + build + + log "Killing old processes" + killall dlv + killall ${SERVICE_NAME} + + start +} + +watch() { + log "Watching for changes" + inotifywait -e "MODIFY,DELETE,MOVED_TO,MOVED_FROM" -m -r ${PWD} | ( + while true; do + read path action file + ext=${file: -3} + if [[ "$ext" == ".go" ]]; then + echo "$file" + fi + done + ) | ( + WAITING="" + while true; do + file="" + read -t 1 file + if test -z "$file"; then + if test ! -z "$WAITING"; then + echo "CHANGED" + WAITING="" + fi + else + log "File ${file} changed" >> ${FILE_CHANGE_LOG_FILE} + WAITING=1 + fi + done + ) | ( + while true; do + read TMP + restart + done + ) +} + +# main part +init +build +start +# watch diff --git a/internal/httpclient-next/go.mod b/internal/httpclient-next/go.mod index 3586a102862..1f452a104c0 100644 --- a/internal/httpclient-next/go.mod +++ b/internal/httpclient-next/go.mod @@ -2,6 +2,4 @@ module github.com/ory/hydra-client-go go 1.13 -require ( - golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 -) +require golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 diff --git a/oauth2/handler.go b/oauth2/handler.go index 0fa913e2061..7119441f92b 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -68,6 +68,8 @@ const ( RevocationPath = "/oauth2/revoke" FlushPath = "/oauth2/flush" DeleteTokensPath = "/oauth2/tokens" // #nosec G101 + + DeviceAuthPath = "/oauth2/device/auth" ) type Handler struct { @@ -107,6 +109,9 @@ func (h *Handler) SetRoutes(admin *x.RouterAdmin, public *x.RouterPublic, corsMi public.Handler("GET", UserinfoPath, corsMiddleware(http.HandlerFunc(h.UserinfoHandler))) public.Handler("POST", UserinfoPath, corsMiddleware(http.HandlerFunc(h.UserinfoHandler))) + public.GET(DeviceAuthPath, h.DeviceAuthHandler) + public.POST(DeviceAuthPath, h.DeviceAuthHandler) + admin.POST(IntrospectPath, h.IntrospectHandler) admin.POST(FlushPath, h.FlushHandler) admin.DELETE(DeleteTokensPath, h.DeleteHandler) @@ -819,3 +824,114 @@ func (h *Handler) DeleteHandler(w http.ResponseWriter, r *http.Request, _ httpro // This function will not be called, OPTIONS request will be handled by cors // this is just a placeholder. func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) {} + +func (h *Handler) DeviceAuthHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + var ctx = r.Context() + + oauthProvider := h.r.OAuth2Provider() + + deviceAuthorizeRequest, err := oauthProvider.NewDeviceAuthorizeRequest(ctx, r) + if err != nil { + x.LogError(r, err, h.r.Logger()) + oauthProvider.WriteDeviceAuthorizeError(w, deviceAuthorizeRequest, err) + return + } + + deviceAuthorizeRequest.SetSession(NewSession("")) + + if len(strings.TrimSpace(deviceAuthorizeRequest.GetRequestForm().Get("link_verifier"))) == 0 { + // Initial Device Authorisation Request. + response, err := oauthProvider.NewDeviceAuthorizeResponse(ctx, deviceAuthorizeRequest) + if err != nil { + x.LogError(r, err, h.r.Logger()) + oauthProvider.WriteDeviceAuthorizeError(w, deviceAuthorizeRequest, err) + return + } + + // Generate the request URL + reqURL := h.c.OAuth2DeviceAuthURL() + reqURL.RawQuery = r.URL.RawQuery + + // Create a Device Link Request, referencing the Device Code generated + linkRequest := &consent.DeviceLinkRequest{ + ID: deviceAuthorizeRequest.GetID(), + DeviceCode: response.GetDeviceCode(), + Verifier: strings.Replace(uuid.New(), "-", "", -1), + RequestedScope: []string(deviceAuthorizeRequest.GetRequestedScopes()), + RequestedAudience: []string(deviceAuthorizeRequest.GetRequestedAudience()), + Client: deviceAuthorizeRequest.GetClient().(*client.Client), + RequestedAt: time.Now().Truncate(time.Second).UTC(), + OpenIDConnectContext: nil, + RequestURL: reqURL.String(), + } + + // Persist the Device Link Request + if err := h.r.ConsentManager().CreateDeviceLinkRequest(r.Context(), linkRequest); err != nil { + x.LogError(r, err, h.r.Logger()) + oauthProvider.WriteDeviceAuthorizeError(w, deviceAuthorizeRequest, err) + return + } + + oauthProvider.WriteDeviceAuthorizeResponse(w, deviceAuthorizeRequest, response) + return + } + + // This must be a follow up request, as part of the usercode/auth/consent redirection journey + + consentSession, err := h.r.ConsentStrategy().HandleOAuth2DeviceAuthorizationRequest(w, r, deviceAuthorizeRequest) + if err != nil { + if errors.Is(err, consent.ErrAbortOAuth2Request) { + x.LogAudit(r, nil, h.r.AuditLogger()) + return // do nothing + } + + x.LogError(r, err, h.r.Logger()) + oauthProvider.WriteDeviceAuthorizeError(w, deviceAuthorizeRequest, err) + return + } + + if consentSession != nil { + // User has completed consent, time to update the device code entity + // Update Link Request with Consent + linkReq, err := h.r.ConsentManager().GetDeviceLinkRequestByVerifier(r.Context(), strings.TrimSpace(deviceAuthorizeRequest.GetRequestForm().Get("link_verifier"))) + if err != nil { + x.LogError(r, err, h.r.Logger()) + oauthProvider.WriteDeviceAuthorizeError(w, deviceAuthorizeRequest, err) + return + } + + deviceCodeSession, err := h.r.OAuth2Storage().GetDeviceCodeSessionByRequestID(r.Context(), linkReq.ID, deviceAuthorizeRequest.GetSession()) + if err != nil { + x.LogError(r, err, h.r.Logger()) + oauthProvider.WriteDeviceAuthorizeError(w, deviceAuthorizeRequest, err) + return + } + + rr, ok := deviceCodeSession.GetSession().(*Session) + if !ok { + // do something with the error + } + + rr.ConsentChallenge = consentSession.ID + deviceCodeSession.SetSession(rr) + + for _, scope := range consentSession.GrantedScope { + deviceCodeSession.GrantScope(scope) + } + + for _, audience := range consentSession.GrantedAudience { + deviceCodeSession.GrantAudience(audience) + } + + err = h.r.OAuth2Provider().AuthorizeDeviceCode(r.Context(), linkReq.DeviceCode, deviceCodeSession) + if err != nil { + x.LogError(r, err, h.r.Logger()) + oauthProvider.WriteDeviceAuthorizeError(w, deviceAuthorizeRequest, err) + return + } + + } + + http.Redirect(w, r, "https://www.google.com", http.StatusFound) +} + diff --git a/oauth2/session.go b/oauth2/session.go index f35e3085fad..e6ed3d078bf 100644 --- a/oauth2/session.go +++ b/oauth2/session.go @@ -130,6 +130,10 @@ func (s *Session) Clone() fosite.Session { return deepcopy.Copy(s).(fosite.Session) } +func (s *Session) GetConsentChallenge() string { + return s.ConsentChallenge +} + var keyRewrites = map[string]string{ "Extra": "extra", "KID": "kid", diff --git a/persistence/sql/migrations/20220728111500000000_create_device_link_request.up.sql b/persistence/sql/migrations/20220728111500000000_create_device_link_request.up.sql new file mode 100644 index 00000000000..609b03dd94d --- /dev/null +++ b/persistence/sql/migrations/20220728111500000000_create_device_link_request.up.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_link_request +( + challenge character varying(40) COLLATE pg_catalog."default" NOT NULL, + verifier character varying(40) COLLATE pg_catalog."default" NOT NULL, + device_code character varying(255) COLLATE pg_catalog."default" NOT NULL, + requested_scope text COLLATE pg_catalog."default" NOT NULL, + requested_at_audience text COLLATE pg_catalog."default" DEFAULT ''::text, + oidc_context text COLLATE pg_catalog."default" NOT NULL, + client_id character varying(255) COLLATE pg_catalog."default" NOT NULL, + login_challenge character varying(40) COLLATE pg_catalog."default", + requested_at timestamp without time zone NOT NULL DEFAULT now(), + request_url text COLLATE pg_catalog."default" NOT NULL, + + CONSTRAINT hydra_oauth2_device_link_request_pkey PRIMARY KEY (challenge), + CONSTRAINT hydra_oauth2_device_link_request_client_id_fk FOREIGN KEY (client_id) + REFERENCES public.hydra_client (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE, + CONSTRAINT hydra_oauth2_device_link_request_login_challenge_fk FOREIGN KEY (login_challenge) + REFERENCES public.hydra_oauth2_authentication_request (challenge) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE SET NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/20220818111500000000_oauth2_device_code.up.sql b/persistence/sql/migrations/20220818111500000000_oauth2_device_code.up.sql new file mode 100644 index 00000000000..ce08ce5b61e --- /dev/null +++ b/persistence/sql/migrations/20220818111500000000_oauth2_device_code.up.sql @@ -0,0 +1,24 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code ( + signature character varying(255) NOT NULL PRIMARY KEY, + user_code character varying(40), + request_id character varying(40) NOT NULL, + requested_at timestamp without time zone NOT NULL DEFAULT now(), + client_id character varying(255) NOT NULL, + scope text NOT NULL, + granted_scope text NOT NULL, + form_data text NOT NULL, + session_data text NOT NULL, + subject character varying(255) NOT NULL, + active boolean NOT NULL, + requested_audience text, + granted_audience text, + challenge_id character varying(40) , + -- CONSTRAINT hydra_oauth2_code_challenge_id_fk FOREIGN KEY (challenge_id) + -- REFERENCES public.hydra_oauth2_consent_request_handled (challenge) MATCH SIMPLE + -- ON UPDATE NO ACTION + -- ON DELETE CASCADE, + CONSTRAINT hydra_oauth2_device_client_id_fk FOREIGN KEY (client_id) + REFERENCES public.hydra_client (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +) diff --git a/persistence/sql/migrations/20220818111500000000_oauth2_user_code.up.sql b/persistence/sql/migrations/20220818111500000000_oauth2_user_code.up.sql new file mode 100644 index 00000000000..147ac07d8b6 --- /dev/null +++ b/persistence/sql/migrations/20220818111500000000_oauth2_user_code.up.sql @@ -0,0 +1,25 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code ( + signature character varying(255) NOT NULL PRIMARY KEY, + user_code character varying(40), + device_code character varying(40), + request_id character varying(40) NOT NULL, + requested_at timestamp without time zone NOT NULL DEFAULT now(), + client_id character varying(255) NOT NULL, + scope text NOT NULL, + granted_scope text NOT NULL, + form_data text NOT NULL, + session_data text NOT NULL, + subject character varying(255) NOT NULL, + active boolean NOT NULL, + requested_audience text, + granted_audience text, + challenge_id character varying(40) , + -- CONSTRAINT hydra_oauth2_code_challenge_id_fk FOREIGN KEY (challenge_id) + -- REFERENCES public.hydra_oauth2_consent_request_handled (challenge) MATCH SIMPLE + -- ON UPDATE NO ACTION + -- ON DELETE CASCADE, + CONSTRAINT hydra_oauth2_device_client_id_fk FOREIGN KEY (client_id) + REFERENCES public.hydra_client (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +) diff --git a/persistence/sql/persister_consent.go b/persistence/sql/persister_consent.go index 923e951fa4a..9cbe7e4bd14 100644 --- a/persistence/sql/persister_consent.go +++ b/persistence/sql/persister_consent.go @@ -532,3 +532,35 @@ func (p *Persister) FlushInactiveLoginConsentRequests(ctx context.Context, notAf return nil } + +func (p *Persister) CreateDeviceLinkRequest(ctx context.Context, req *consent.DeviceLinkRequest) error { + return errorsx.WithStack(p.Connection(ctx).Create(req)) +} + +func (p *Persister) GetDeviceLinkRequest(ctx context.Context, challenge string) (*consent.DeviceLinkRequest, error) { + var lr consent.DeviceLinkRequest + return &lr, p.transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + if err := (&lr).FindInDB(c, challenge); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return errorsx.WithStack(x.ErrNotFound) + } + return sqlcon.HandleError(err) + } + + return nil + }) +} + +func (p *Persister) GetDeviceLinkRequestByVerifier(ctx context.Context, verifier string) (*consent.DeviceLinkRequest, error) { + var lr consent.DeviceLinkRequest + return &lr, p.transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + if err := c.Select(lr.TableName()+".*").Where("verifier = ?", verifier).First(&lr); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return errorsx.WithStack(x.ErrNotFound) + } + return sqlcon.HandleError(err) + } + + return nil + }) +} diff --git a/persistence/sql/persister_oauth2.go b/persistence/sql/persister_oauth2.go index 2ef98f7481c..a9f3efd58f9 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -50,11 +50,13 @@ type ( ) const ( - sqlTableOpenID tableName = "oidc" - sqlTableAccess tableName = "access" - sqlTableRefresh tableName = "refresh" - sqlTableCode tableName = "code" - sqlTablePKCE tableName = "pkce" + sqlTableOpenID tableName = "oidc" + sqlTableAccess tableName = "access" + sqlTableRefresh tableName = "refresh" + sqlTableCode tableName = "code" + sqlTableDeviceCode tableName = "device_code" + sqlTableUserCode tableName = "user_code" + sqlTablePKCE tableName = "pkce" ) func (r OAuth2RequestSQL) TableName() string { @@ -148,6 +150,7 @@ func (r *OAuth2RequestSQL) toRequest(ctx context.Context, session fosite.Session GrantedAudience: stringsx.Splitx(r.GrantedAudience, "|"), Form: val, Session: session, + ConsentGranted: r.ConsentChallenge.Valid, }, nil } @@ -221,6 +224,21 @@ func (p *Persister) createSession(ctx context.Context, signature string, request return nil } +func (p *Persister) updateSession(ctx context.Context, signature string, requester fosite.Requester, table tableName) error { + req, err := p.sqlSchemaFromRequest(signature, requester, table) + if err != nil { + return err + } + + if err := sqlcon.HandleError(p.Connection(ctx).Update(req)); errors.Is(err, sqlcon.ErrConcurrentUpdate) { + return errors.Wrap(fosite.ErrSerializationFailure, err.Error()) + } else if err != nil { + return err + } + return nil +} + + func (p *Persister) findSessionBySignature(ctx context.Context, rawSignature string, session fosite.Session, table tableName) (fosite.Requester, error) { rawSignature = p.hashSignature(rawSignature, table) @@ -237,8 +255,14 @@ func (p *Persister) findSessionBySignature(ctx context.Context, rawSignature str fr, err = r.toRequest(ctx, session, p) if err != nil { return err - } else if table == sqlTableCode { + } + switch table { + case sqlTableCode: return errorsx.WithStack(fosite.ErrInvalidatedAuthorizeCode) + case sqlTableDeviceCode: + return errorsx.WithStack(fosite.ErrInvalidatedDeviceCode) + case sqlTableUserCode: + return errorsx.WithStack(fosite.ErrInvalidatedUserCode) } return errorsx.WithStack(fosite.ErrInactiveToken) @@ -249,6 +273,37 @@ func (p *Persister) findSessionBySignature(ctx context.Context, rawSignature str }) } +func (p *Persister) findSessionByRequestID(ctx context.Context, requestID string, session fosite.Session, table tableName) (fosite.Requester, error) { + r := OAuth2RequestSQL{Table: table} + var fr fosite.Requester + + return fr, p.transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + err := p.Connection(ctx).Where("request_id = ?", requestID).First(&r) + if errors.Is(err, sql.ErrNoRows) { + return errorsx.WithStack(fosite.ErrNotFound) + } else if err != nil { + return sqlcon.HandleError(err) + } else if !r.Active { + fr, err = r.toRequest(ctx, session, p) + if err != nil { + return err + } + switch table { + case sqlTableCode: + return errorsx.WithStack(fosite.ErrInvalidatedAuthorizeCode) + case sqlTableDeviceCode: + return errorsx.WithStack(fosite.ErrInvalidatedDeviceCode) + case sqlTableUserCode: + return errorsx.WithStack(fosite.ErrInvalidatedUserCode) + } + + return errorsx.WithStack(fosite.ErrInactiveToken) + } + fr, err = r.toRequest(ctx, session, p) + return err + }) +} + func (p *Persister) deleteSessionBySignature(ctx context.Context, signature string, table tableName) error { signature = p.hashSignature(signature, table) @@ -316,6 +371,92 @@ func (p *Persister) InvalidateAuthorizeCodeSession(ctx context.Context, signatur Exec()) } +//func (p *Persister) CreateDeviceAuthorizeSession(ctx context.Context, deviceSignature string, userSignature string, request fosite.Requester) (err error) { +// req, err := p.sqlSchemaFromRequest(deviceSignature, request, sqlTableDevice) +// if err != nil { +// return err +// } + +// deviceAuthRequest := &OAuth2DeviceRequestSQL{ +// ID: req.ID, +// UserCode: userSignature, +// Request: req.Request, +// ConsentChallenge: req.ConsentChallenge, +// RequestedAt: req.RequestedAt, +// Client: req.Client, +// Scopes: req.Scopes, +// GrantedScope: req.GrantedScope, +// GrantedAudience: req.GrantedAudience, +// RequestedAudience: req.RequestedAudience, +// Form: req.Form, +// Session: req.Session, +// Subject: req.Subject, +// Active: req.Active, +// Table: req.Table, +// } + +// if err := sqlcon.HandleError(p.Connection(ctx).Create(deviceAuthRequest)); errors.Is(err, sqlcon.ErrConcurrentUpdate) { +// return errors.Wrap(fosite.ErrSerializationFailure, err.Error()) +// } else if err != nil { +// return err +// } +// return nil +// } + +// func (p *Persister) GetDeviceAuthorizeSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) { +// return p.findSessionBySignature(ctx, signature, session, sqlTableDevice) +// } + +func (p *Persister) CreateDeviceCodeSession(ctx context.Context, signature string, requester fosite.Requester) (err error) { + return p.createSession(ctx, signature, requester, sqlTableDeviceCode) +} + +func (p *Persister) UpdateDeviceCodeSession(ctx context.Context, signature string, requester fosite.Requester) (err error) { + return p.updateSession(ctx, signature, requester, sqlTableDeviceCode) +} + +func (p *Persister) GetDeviceCodeSession(ctx context.Context, signature string, session fosite.Session) (request fosite.DeviceAuthorizeRequester, err error) { + requester, err := p.findSessionBySignature(ctx, signature, session, sqlTableDeviceCode) + if err != nil { + return nil, err + } + + return &fosite.DeviceAuthorizeRequest{ + AuthorizationPending: requester.GetSession().(*oauth2.Session).ConsentChallenge == "", + Request: *requester.(*fosite.Request), + }, nil +} + +func (p *Persister) GetDeviceCodeSessionByRequestID(ctx context.Context, requestID string, session fosite.Session) (request fosite.DeviceAuthorizeRequester, err error) { + requester, err := p.findSessionByRequestID(ctx, requestID, session, sqlTableDeviceCode) + if err != nil { + return nil, err + } + + return &fosite.DeviceAuthorizeRequest{ + AuthorizationPending: requester.GetSession().(*oauth2.Session).ConsentChallenge == "", + Request: *requester.(*fosite.Request), + }, nil +} + +func (p *Persister) CreateUserCodeSession(ctx context.Context, signature string, requester fosite.Requester) (err error) { + return p.createSession(ctx, signature, requester, sqlTableUserCode) +} + +func (p *Persister) GetUserCodeSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) { + return p.findSessionBySignature(ctx, signature, session, sqlTableUserCode) +} + +func (p *Persister) InvalidateUserCodeSession(ctx context.Context, signature string) (err error) { + /* #nosec G201 table is static */ + return sqlcon.HandleError( + p.Connection(ctx). + RawQuery( + fmt.Sprintf("UPDATE %s SET active=false WHERE signature=?", OAuth2RequestSQL{Table: sqlTableUserCode}.TableName()), + signature). + Exec()) +} + func (p *Persister) CreateAccessTokenSession(ctx context.Context, signature string, requester fosite.Requester) (err error) { return p.createSession(ctx, signature, requester, sqlTableAccess) } diff --git a/spec/config.json b/spec/config.json index 7bc3a8fa8af..cbd109f8a31 100644 --- a/spec/config.json +++ b/spec/config.json @@ -623,6 +623,14 @@ "format": "uri", "examples": ["https://my-consent.app/consent"] }, + "device_link": { + "type": "string", + "description": "Sets the device link endpoint of the User Login & Consent flow when supporting the Device Authorisation Grant. Defaults to an internal fallback URL showing an error.", + "format": "uri", + "examples": [ + "https://my-device-link.app/link" + ] + }, "logout": { "type": "string", "description": "Sets the logout endpoint. Defaults to an internal fallback URL showing an error.", @@ -724,6 +732,17 @@ "type": "object", "additionalProperties": false, "properties": { + "device_authorization": { + "type": "object", + "properties": { + "token_polling_interval": { + "type": "integer", + "default": 2, + "title": "Device Authorisation Token Polling Interval", + "description": "The minimum amount of time in seconds that the client SHOULD wait between polling requests to the token endpoint." + } + } + }, "expose_internal_errors": { "type": "boolean", "description": "Set this to true if you want to share error debugging information with your OAuth 2.0 clients. Keep in mind that debug information is very valuable when dealing with errors, but might also expose database error codes and similar errors.", diff --git a/x/fosite_storer.go b/x/fosite_storer.go index 4ca3677b1cf..9074848917b 100644 --- a/x/fosite_storer.go +++ b/x/fosite_storer.go @@ -34,6 +34,8 @@ import ( type FositeStorer interface { fosite.Storage oauth2.CoreStorage + oauth2.DeviceCodeStorage + oauth2.UserCodeStorage openid.OpenIDConnectRequestStorage pkce.PKCERequestStorage rfc7523.RFC7523KeyStorage