From 75f503ce43fe394b98c4b922af30f8dd2dca964b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 27 Feb 2023 12:55:52 +0100 Subject: [PATCH] implement rp --- pkg/client/client.go | 82 ++++++++++++++++++++++++++++++-- pkg/client/rp/device.go | 63 ++++++++++++++++++++---- pkg/client/rp/relying_party.go | 32 +++++++------ pkg/oidc/device_authorization.go | 4 +- 4 files changed, 153 insertions(+), 28 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index 08e16fd..b9ae008 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1,6 +1,8 @@ package client import ( + "context" + "encoding/json" "errors" "fmt" "io" @@ -188,18 +190,92 @@ func SignedJWTProfileAssertion(clientID string, audience []string, expiration ti } type DeviceAuthorizationCaller interface { - GetDeviceCodeEndpoint() string + GetDeviceAuthorizationEndpoint() string HttpClient() *http.Client } -func CallDeviceAuthorizationEndpoint(request interface{}, caller DeviceAuthorizationCaller) (*oidc.DeviceAuthorizationResponse, error) { - req, err := httphelper.FormRequest(caller.GetDeviceCodeEndpoint(), request, Encoder, nil) +func CallDeviceAuthorizationEndpoint(request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller) (*oidc.DeviceAuthorizationResponse, error) { + req, err := httphelper.FormRequest(caller.GetDeviceAuthorizationEndpoint(), request, Encoder, nil) if err != nil { return nil, err } + if request.ClientSecret != "" { + req.SetBasicAuth(request.ClientID, request.ClientSecret) + } + resp := new(oidc.DeviceAuthorizationResponse) if err := httphelper.HttpRequest(caller.HttpClient(), req, &resp); err != nil { return nil, err } return resp, nil } + +type DeviceAccessTokenRequest struct { + *oidc.ClientCredentialsRequest + oidc.DeviceAccessTokenRequest +} + +func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) { + req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, nil) + if err != nil { + return nil, err + } + if request.ClientSecret != "" { + req.SetBasicAuth(request.ClientID, request.ClientSecret) + } + + httpResp, err := caller.HttpClient().Do(req) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + + resp := new(struct { + *oidc.AccessTokenResponse + *oidc.Error + }) + if err = json.NewDecoder(httpResp.Body).Decode(resp); err != nil { + return nil, err + } + + if httpResp.StatusCode == http.StatusOK { + return resp.AccessTokenResponse, nil + } + + return nil, resp.Error +} + +func PollDeviceAccessTokenEndpoint(ctx context.Context, interval time.Duration, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) { + for { + timer := time.After(interval) + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-timer: + } + + ctx, cancel := context.WithTimeout(ctx, interval) + defer cancel() + + resp, err := CallDeviceAccessTokenEndpoint(ctx, request, caller) + if err == nil { + return resp, nil + } + if errors.Is(err, context.DeadlineExceeded) { + interval += 5 * time.Second + } + var target *oidc.Error + if !errors.As(err, &target) { + return nil, err + } + switch target.ErrorType { + case oidc.AuthorizationPending: + continue + case oidc.SlowDown: + interval += 5 * time.Second + continue + default: + return nil, err + } + } +} diff --git a/pkg/client/rp/device.go b/pkg/client/rp/device.go index c7d0be8..2278eca 100644 --- a/pkg/client/rp/device.go +++ b/pkg/client/rp/device.go @@ -1,20 +1,67 @@ package rp import ( + "context" + "errors" + "fmt" + "time" + "github.com/zitadel/oidc/v2/pkg/client" "github.com/zitadel/oidc/v2/pkg/oidc" ) -func DeviceAuthorization(clientID string, scopes []string, rp RelyingParty) (*oidc.DeviceAuthorizationResponse, error) { - req := &oidc.DeviceAuthorizationRequest{ - Scopes: scopes, - ClientID: clientID, +func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) { + confg := rp.OAuthConfig() + req := &oidc.ClientCredentialsRequest{ + GrantType: oidc.GrantTypeDeviceCode, + Scope: scopes, + ClientID: confg.ClientID, + ClientSecret: confg.ClientSecret, } + + if signer := rp.Signer(); signer != nil { + assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, signer) + if err != nil { + return nil, fmt.Errorf("failed to build assertion: %w", err) + } + req.ClientAssertion = assertion + req.ClientAssertionType = oidc.ClientAssertionTypeJWTAssertion + } + + return req, nil +} + +// DeviceAuthorization starts a new Device Authorization flow as defined +// in RFC 8628, section 3.1 and 3.2: +// https://www.rfc-editor.org/rfc/rfc8628#section-3.1 +func DeviceAuthorization(scopes []string, rp RelyingParty) (*oidc.DeviceAuthorizationResponse, error) { + req, err := newDeviceClientCredentialsRequest(scopes, rp) + if err != nil { + return nil, err + } + return client.CallDeviceAuthorizationEndpoint(req, rp) } -/* -func DeviceAccessToken() (*oauth2.Token, error) { - req := &oidc.DeviceAccessTokenRequest{} +// DeviceAccessToken attempts to obtain tokens from a Device Authorization, +// by means of polling as defined in RFC, section 3.3 and 3.4: +// https://www.rfc-editor.org/rfc/rfc8628#section-3.4 +func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) { + caller, ok := rp.(client.TokenEndpointCaller) + if !ok { + return nil, errors.New("rp does not implement TokenEndPointCaller") + } + req := &client.DeviceAccessTokenRequest{ + DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{ + GrantType: oidc.GrantTypeDeviceCode, + DeviceCode: deviceCode, + }, + } + + req.ClientCredentialsRequest, err = newDeviceClientCredentialsRequest(nil, rp) + if err != nil { + return nil, err + } + + return client.PollDeviceAccessTokenEndpoint(ctx, interval, req, caller) } -*/ diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index d44f78a..96fe219 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -59,7 +59,9 @@ type RelyingParty interface { // UserinfoEndpoint returns the userinfo UserinfoEndpoint() string - GetDeviceCodeEndpoint() string + // GetDeviceAuthorizationEndpoint returns the enpoint which can + // be used to start a DeviceAuthorization flow. + GetDeviceAuthorizationEndpoint() string // IDTokenVerifier returns the verifier interface used for oidc id_token verification IDTokenVerifier() IDTokenVerifier @@ -123,8 +125,8 @@ func (rp *relyingParty) UserinfoEndpoint() string { return rp.endpoints.UserinfoURL } -func (rp *relyingParty) GetDeviceCodeEndpoint() string { - return rp.endpoints.DeviceCodeURL +func (rp *relyingParty) GetDeviceAuthorizationEndpoint() string { + return rp.endpoints.DeviceAuthorizationURL } func (rp *relyingParty) GetEndSessionEndpoint() string { @@ -501,12 +503,12 @@ type OptionFunc func(RelyingParty) type Endpoints struct { oauth2.Endpoint - IntrospectURL string - UserinfoURL string - JKWsURL string - EndSessionURL string - RevokeURL string - DeviceCodeURL string + IntrospectURL string + UserinfoURL string + JKWsURL string + EndSessionURL string + RevokeURL string + DeviceAuthorizationURL string } func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { @@ -516,12 +518,12 @@ func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { AuthStyle: oauth2.AuthStyleAutoDetect, TokenURL: discoveryConfig.TokenEndpoint, }, - IntrospectURL: discoveryConfig.IntrospectionEndpoint, - UserinfoURL: discoveryConfig.UserinfoEndpoint, - JKWsURL: discoveryConfig.JwksURI, - EndSessionURL: discoveryConfig.EndSessionEndpoint, - RevokeURL: discoveryConfig.RevocationEndpoint, - DeviceCodeURL: discoveryConfig.DeviceAuthorizationEndpoint, + IntrospectURL: discoveryConfig.IntrospectionEndpoint, + UserinfoURL: discoveryConfig.UserinfoEndpoint, + JKWsURL: discoveryConfig.JwksURI, + EndSessionURL: discoveryConfig.EndSessionEndpoint, + RevokeURL: discoveryConfig.RevocationEndpoint, + DeviceAuthorizationURL: discoveryConfig.DeviceAuthorizationEndpoint, } } diff --git a/pkg/oidc/device_authorization.go b/pkg/oidc/device_authorization.go index 8ff8cee..969d528 100644 --- a/pkg/oidc/device_authorization.go +++ b/pkg/oidc/device_authorization.go @@ -24,6 +24,6 @@ type DeviceAuthorizationResponse struct { // https://www.rfc-editor.org/rfc/rfc8628#section-3.4, // Device Access Token Request. type DeviceAccessTokenRequest struct { - GrantType string `json:"grant_type"` - DeviceCode string `json:"device_code"` + GrantType GrantType `json:"grant_type"` + DeviceCode string `json:"device_code"` }