implement rp

This commit is contained in:
Tim Möhlmann 2023-02-27 12:55:52 +01:00
parent b885398466
commit 75f503ce43
4 changed files with 153 additions and 28 deletions

View file

@ -1,6 +1,8 @@
package client package client
import ( import (
"context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -188,18 +190,92 @@ func SignedJWTProfileAssertion(clientID string, audience []string, expiration ti
} }
type DeviceAuthorizationCaller interface { type DeviceAuthorizationCaller interface {
GetDeviceCodeEndpoint() string GetDeviceAuthorizationEndpoint() string
HttpClient() *http.Client HttpClient() *http.Client
} }
func CallDeviceAuthorizationEndpoint(request interface{}, caller DeviceAuthorizationCaller) (*oidc.DeviceAuthorizationResponse, error) { func CallDeviceAuthorizationEndpoint(request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller) (*oidc.DeviceAuthorizationResponse, error) {
req, err := httphelper.FormRequest(caller.GetDeviceCodeEndpoint(), request, Encoder, nil) req, err := httphelper.FormRequest(caller.GetDeviceAuthorizationEndpoint(), request, Encoder, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if request.ClientSecret != "" {
req.SetBasicAuth(request.ClientID, request.ClientSecret)
}
resp := new(oidc.DeviceAuthorizationResponse) resp := new(oidc.DeviceAuthorizationResponse)
if err := httphelper.HttpRequest(caller.HttpClient(), req, &resp); err != nil { if err := httphelper.HttpRequest(caller.HttpClient(), req, &resp); err != nil {
return nil, err return nil, err
} }
return resp, nil 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
}
}
}

View file

@ -1,20 +1,67 @@
package rp package rp
import ( import (
"context"
"errors"
"fmt"
"time"
"github.com/zitadel/oidc/v2/pkg/client" "github.com/zitadel/oidc/v2/pkg/client"
"github.com/zitadel/oidc/v2/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
func DeviceAuthorization(clientID string, scopes []string, rp RelyingParty) (*oidc.DeviceAuthorizationResponse, error) { func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) {
req := &oidc.DeviceAuthorizationRequest{ confg := rp.OAuthConfig()
Scopes: scopes, req := &oidc.ClientCredentialsRequest{
ClientID: clientID, 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) return client.CallDeviceAuthorizationEndpoint(req, rp)
} }
/* // DeviceAccessToken attempts to obtain tokens from a Device Authorization,
func DeviceAccessToken() (*oauth2.Token, error) { // by means of polling as defined in RFC, section 3.3 and 3.4:
req := &oidc.DeviceAccessTokenRequest{} // 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)
} }
*/

View file

@ -59,7 +59,9 @@ type RelyingParty interface {
// UserinfoEndpoint returns the userinfo // UserinfoEndpoint returns the userinfo
UserinfoEndpoint() string 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 returns the verifier interface used for oidc id_token verification
IDTokenVerifier() IDTokenVerifier IDTokenVerifier() IDTokenVerifier
@ -123,8 +125,8 @@ func (rp *relyingParty) UserinfoEndpoint() string {
return rp.endpoints.UserinfoURL return rp.endpoints.UserinfoURL
} }
func (rp *relyingParty) GetDeviceCodeEndpoint() string { func (rp *relyingParty) GetDeviceAuthorizationEndpoint() string {
return rp.endpoints.DeviceCodeURL return rp.endpoints.DeviceAuthorizationURL
} }
func (rp *relyingParty) GetEndSessionEndpoint() string { func (rp *relyingParty) GetEndSessionEndpoint() string {
@ -506,7 +508,7 @@ type Endpoints struct {
JKWsURL string JKWsURL string
EndSessionURL string EndSessionURL string
RevokeURL string RevokeURL string
DeviceCodeURL string DeviceAuthorizationURL string
} }
func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
@ -521,7 +523,7 @@ func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
JKWsURL: discoveryConfig.JwksURI, JKWsURL: discoveryConfig.JwksURI,
EndSessionURL: discoveryConfig.EndSessionEndpoint, EndSessionURL: discoveryConfig.EndSessionEndpoint,
RevokeURL: discoveryConfig.RevocationEndpoint, RevokeURL: discoveryConfig.RevocationEndpoint,
DeviceCodeURL: discoveryConfig.DeviceAuthorizationEndpoint, DeviceAuthorizationURL: discoveryConfig.DeviceAuthorizationEndpoint,
} }
} }

View file

@ -24,6 +24,6 @@ type DeviceAuthorizationResponse struct {
// https://www.rfc-editor.org/rfc/rfc8628#section-3.4, // https://www.rfc-editor.org/rfc/rfc8628#section-3.4,
// Device Access Token Request. // Device Access Token Request.
type DeviceAccessTokenRequest struct { type DeviceAccessTokenRequest struct {
GrantType string `json:"grant_type"` GrantType GrantType `json:"grant_type"`
DeviceCode string `json:"device_code"` DeviceCode string `json:"device_code"`
} }