From d5197e0a53361d8c26cdacf7035cf31e9f9ed90e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 15 Dec 2023 14:12:18 +0200 Subject: [PATCH] deviceState implement IDTokenRequest directly --- pkg/op/device.go | 74 ++++++++++++++++++++++++++++------------- pkg/op/device_test.go | 63 ++++++++++++++++------------------- pkg/op/server_legacy.go | 7 +--- pkg/op/storage.go | 9 ----- pkg/op/token.go | 2 ++ 5 files changed, 82 insertions(+), 73 deletions(-) diff --git a/pkg/op/device.go b/pkg/op/device.go index 5226aef2..2d4f0f35 100644 --- a/pkg/op/device.go +++ b/pkg/op/device.go @@ -186,24 +186,6 @@ func NewUserCode(charSet []rune, charAmount, dashInterval int) (string, error) { return buf.String(), nil } -type deviceAccessTokenRequest struct { - subject string - audience []string - scopes []string -} - -func (r *deviceAccessTokenRequest) GetSubject() string { - return r.subject -} - -func (r *deviceAccessTokenRequest) GetAudience() []string { - return r.audience -} - -func (r *deviceAccessTokenRequest) GetScopes() []string { - return r.scopes -} - func DeviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { ctx, span := tracer.Start(r.Context(), "DeviceAccessToken") defer span.End() @@ -230,7 +212,7 @@ func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchang if err != nil { return err } - state, err := CheckDeviceAuthorizationState(ctx, clientID, req.DeviceCode, exchanger) + tokenRequest, err := CheckDeviceAuthorizationState(ctx, clientID, req.DeviceCode, exchanger) if err != nil { return err } @@ -244,11 +226,6 @@ func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchang WithDescription("confidential client requires authentication") } - tokenRequest := &deviceAccessTokenRequest{ - subject: state.Subject, - audience: []string{clientID}, - scopes: state.Scopes, - } resp, err := CreateDeviceTokenResponse(r.Context(), tokenRequest, exchanger, client) if err != nil { return err @@ -266,6 +243,50 @@ func ParseDeviceAccessTokenRequest(r *http.Request, exchanger Exchanger) (*oidc. return req, nil } +// DeviceAuthorizationState describes the current state of +// the device authorization flow. +// It implements the [IDTokenRequest] interface. +type DeviceAuthorizationState struct { + ClientID string + Audience []string + Scopes []string + Expires time.Time // The time after we consider the authorization request timed-out + Done bool // The user authenticated and approved the authorization request + Denied bool // The user authenticated and denied the authorization request + + // The following fields are populated after Done == true + Subject string + AMR []string + AuthTime time.Time +} + +func (r *DeviceAuthorizationState) GetAMR() []string { + return r.AMR +} + +func (r *DeviceAuthorizationState) GetAudience() []string { + if !slices.Contains(r.Audience, r.ClientID) { + r.Audience = append(r.Audience, r.ClientID) + } + return r.Audience +} + +func (r *DeviceAuthorizationState) GetAuthTime() time.Time { + return r.AuthTime +} + +func (r *DeviceAuthorizationState) GetClientID() string { + return r.ClientID +} + +func (r *DeviceAuthorizationState) GetScopes() []string { + return r.Scopes +} + +func (r *DeviceAuthorizationState) GetSubject() string { + return r.Subject +} + func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) { storage, err := assertDeviceStorage(exchanger.Storage()) if err != nil { @@ -292,6 +313,10 @@ func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode str } func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client Client) (*oidc.AccessTokenResponse, error) { + /* TODO(v4): + Change the TokenRequest argument type to *DeviceAuthorizationState. + Breaking change that can not be done for v3. + */ ctx, span := tracer.Start(ctx, "CreateDeviceTokenResponse") defer span.End() @@ -307,6 +332,7 @@ func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, c ExpiresIn: uint64(validity.Seconds()), } + // TODO(v4): remove type assertion if idTokenRequest, ok := tokenRequest.(IDTokenRequest); ok && slices.Contains(tokenRequest.GetScopes(), oidc.ScopeOpenID) { response.IDToken, err = CreateIDToken(ctx, IssuerFromContext(ctx), idTokenRequest, client.IDTokenLifetime(), accessToken, "", creator.Storage(), client) if err != nil { diff --git a/pkg/op/device_test.go b/pkg/op/device_test.go index 2400598a..570b943e 100644 --- a/pkg/op/device_test.go +++ b/pkg/op/device_test.go @@ -465,50 +465,46 @@ func TestCreateDeviceTokenResponse(t *testing.T) { }{ { name: "access token", - tokenRequest: &storage.AuthRequest{ - ID: "auth1", - AuthTime: time.Now(), - ApplicationID: "app1", - ResponseType: oidc.ResponseTypeCode, - UserID: "id1", + tokenRequest: &op.DeviceAuthorizationState{ + ClientID: "client1", + Subject: "id1", + AMR: []string{"password"}, + AuthTime: time.Now(), }, wantAccessToken: true, }, { name: "access and refresh tokens", - tokenRequest: &storage.AuthRequest{ - ID: "auth1", - AuthTime: time.Now(), - ApplicationID: "app1", - ResponseType: oidc.ResponseTypeCode, - UserID: "id1", - Scopes: []string{oidc.ScopeOfflineAccess}, + tokenRequest: &op.DeviceAuthorizationState{ + ClientID: "client1", + Subject: "id1", + AMR: []string{"password"}, + AuthTime: time.Now(), + Scopes: []string{oidc.ScopeOfflineAccess}, }, wantAccessToken: true, wantRefreshToken: true, }, { name: "access and id token", - tokenRequest: &storage.AuthRequest{ - ID: "auth1", - AuthTime: time.Now(), - ApplicationID: "app1", - ResponseType: oidc.ResponseTypeCode, - UserID: "id1", - Scopes: []string{oidc.ScopeOpenID}, + tokenRequest: &op.DeviceAuthorizationState{ + ClientID: "client1", + Subject: "id1", + AMR: []string{"password"}, + AuthTime: time.Now(), + Scopes: []string{oidc.ScopeOpenID}, }, wantAccessToken: true, wantIDToken: true, }, { name: "access, refresh and id token", - tokenRequest: &storage.AuthRequest{ - ID: "auth1", - AuthTime: time.Now(), - ApplicationID: "app1", - ResponseType: oidc.ResponseTypeCode, - UserID: "id1", - Scopes: []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID}, + tokenRequest: &op.DeviceAuthorizationState{ + ClientID: "client1", + Subject: "id1", + AMR: []string{"password"}, + AuthTime: time.Now(), + Scopes: []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID}, }, wantAccessToken: true, wantRefreshToken: true, @@ -516,13 +512,12 @@ func TestCreateDeviceTokenResponse(t *testing.T) { }, { name: "id token creation error", - tokenRequest: &storage.AuthRequest{ - ID: "auth1", - AuthTime: time.Now(), - ApplicationID: "app1", - ResponseType: oidc.ResponseTypeCode, - UserID: "foobar", - Scopes: []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID}, + tokenRequest: &op.DeviceAuthorizationState{ + ClientID: "client1", + Subject: "foobar", + AMR: []string{"password"}, + AuthTime: time.Now(), + Scopes: []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID}, }, wantErr: true, }, diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go index 4b84c71b..114d431d 100644 --- a/pkg/op/server_legacy.go +++ b/pkg/op/server_legacy.go @@ -299,15 +299,10 @@ func (s *LegacyServer) DeviceToken(ctx context.Context, r *ClientRequest[oidc.De ctx, cancel := context.WithTimeout(ctx, 4*time.Second) defer cancel() - state, err := CheckDeviceAuthorizationState(ctx, r.Client.GetID(), r.Data.DeviceCode, s.provider) + tokenRequest, err := CheckDeviceAuthorizationState(ctx, r.Client.GetID(), r.Data.DeviceCode, s.provider) if err != nil { return nil, err } - tokenRequest := &deviceAccessTokenRequest{ - subject: state.Subject, - audience: []string{r.Client.GetID()}, - scopes: state.Scopes, - } resp, err := CreateDeviceTokenResponse(ctx, tokenRequest, s.provider, r.Client) if err != nil { return nil, err diff --git a/pkg/op/storage.go b/pkg/op/storage.go index d083a31c..a1a00ed4 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -168,15 +168,6 @@ type EndSessionRequest struct { var ErrDuplicateUserCode = errors.New("user code already exists") -type DeviceAuthorizationState struct { - ClientID string - Scopes []string - Expires time.Time - Done bool - Subject string - Denied bool -} - type DeviceAuthorizationStorage interface { // StoreDeviceAuthorizationRequest stores a new device authorization request in the database. // User code will be used by the user to complete the login flow and must be unique. diff --git a/pkg/op/token.go b/pkg/op/token.go index 63a01a6c..83889f02 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -84,6 +84,8 @@ func needsRefreshToken(tokenRequest TokenRequest, client AccessTokenClient) bool return req.GetRequestedTokenType() == oidc.RefreshTokenType case RefreshTokenRequest: return true + case *DeviceAuthorizationState: + return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && ValidateGrantType(client, oidc.GrantTypeRefreshToken) default: return false }