Skip to content

Commit

Permalink
Implement PAR on the authentication client
Browse files Browse the repository at this point in the history
  • Loading branch information
ewanharris committed Dec 7, 2023
1 parent 6faf79f commit 93053ce
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 0 deletions.
44 changes: 44 additions & 0 deletions authentication/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,44 @@ func (o *OAuth) RevokeRefreshToken(ctx context.Context, body oauth.RevokeRefresh
return o.authentication.Request(ctx, "POST", o.authentication.URI("oauth", "revoke"), body, nil, opts...)
}

// PushedAuthorization performs a Pushed Authorization Request that can be used to initiate an OAuth flow from
// the backchannel instead of building a URL.
//
// See: https://www.rfc-editor.org/rfc/rfc9126.html
func (o *OAuth) PushedAuthorization(ctx context.Context, body oauth.PushedAuthorizationRequest, opts ...RequestOption) (p *oauth.PushedAuthorizationRequestResponse, err error) {
data := url.Values{
"response_type": []string{body.ResponseType},
"redirect_uri": []string{body.RedirectURI},
}

addIfNotEmpty("scope", body.Scope, data)
addIfNotEmpty("audience", body.Audience, data)
addIfNotEmpty("nonce", body.Nonce, data)
addIfNotEmpty("response_mode", body.ResponseMode, data)
addIfNotEmpty("organization", body.Organization, data)
addIfNotEmpty("invitation", body.Invitation, data)
addIfNotEmpty("connection", body.Connection, data)
addIfNotEmpty("code_challenge", body.CodeChallenge, data)

for key, value := range body.ExtraParameters {
data.Set(key, value)
}

err = o.addClientAuthentication(body.ClientAuthentication, data, true)

if err != nil {
return nil, err
}

err = o.authentication.Request(ctx, "POST", o.authentication.URI("oauth", "par"), data, &p, opts...)

if err != nil {
return nil, err
}

return
}

func (o *OAuth) addClientAuthentication(params oauth.ClientAuthentication, body url.Values, required bool) error {
clientID := params.ClientID
if params.ClientID == "" {
Expand Down Expand Up @@ -289,3 +327,9 @@ func createClientAssertion(clientAssertionSigningAlg, clientAssertionSigningKey,

return string(b), nil
}

func addIfNotEmpty(key string, value string, qs url.Values) {
if value != "" {
qs.Set(key, value)
}
}
33 changes: 33 additions & 0 deletions authentication/oauth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,36 @@ type IDTokenValidationOptions struct {
Nonce string
Organization string
}

// PushedAuthorizationRequest defines the request body for performing a Pushed Authorization Request (PAR).
type PushedAuthorizationRequest struct {
ClientAuthentication
// The URI to redirect to.
RedirectURI string
// Scopes to request.
Scope string
// The unique identifier of the target API you want to access.
Audience string
// The nonce.
Nonce string
// The response mode to use.
ResponseMode string
// The response type the client expects.
ResponseType string
// The organization to log the user in to.
Organization string
// The ID of an invitation to accept.
Invitation string
// Name of the connection.
Connection string
// A Base64-encoded SHA-256 hash of the code_verifier used for the Authorization Code Flow with PKCE.
CodeChallenge string
// Extra parameters to be added to the request. Values set here will override any existing values.
ExtraParameters map[string]string
}

// PushedAuthorizationRequestResponse defines the response when performing a Pushed Authorization Request.
type PushedAuthorizationRequestResponse struct {
RequestURI string `json:"request_uri,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"`
}
52 changes: 52 additions & 0 deletions authentication/oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,58 @@ func TestOAuthWithIDTokenVerification(t *testing.T) {
})
}

func TestPushedAuthorizationRequest(t *testing.T) {
t.Run("Should require a client secret", func(t *testing.T) {
_, err := authAPI.OAuth.PushedAuthorization(context.Background(), oauth.PushedAuthorizationRequest{})
assert.ErrorContains(t, err, "client_secret or client_assertion is required but not provided")
})

t.Run("Should make a PAR request", func(t *testing.T) {
skipE2E(t)
configureHTTPTestRecordings(t, authAPI)

res, err := authAPI.OAuth.PushedAuthorization(context.Background(), oauth.PushedAuthorizationRequest{
ClientAuthentication: oauth.ClientAuthentication{
ClientSecret: clientSecret,
},
ResponseType: "code",
RedirectURI: "http://localhost:3000/callback",
})

require.NoError(t, err)
assert.NotEmpty(t, res.RequestURI)
assert.NotEmpty(t, res.ExpiresIn)
})

t.Run("Should support all arguments", func(t *testing.T) {
skipE2E(t)
configureHTTPTestRecordings(t, authAPI)

res, err := authAPI.OAuth.PushedAuthorization(context.Background(), oauth.PushedAuthorizationRequest{
ClientAuthentication: oauth.ClientAuthentication{
ClientSecret: clientSecret,
},
ResponseType: "code",
RedirectURI: "http://localhost:3000/callback",
Audience: "test-audience",
Nonce: "abc123",
ResponseMode: "form_post",
Scope: "openid profile email",
Organization: "my-org",
Invitation: "invite",
Connection: "Username-Password",
CodeChallenge: "n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg",
ExtraParameters: map[string]string{
"test": "value",
},
})

require.NoError(t, err)
assert.NotEmpty(t, res.RequestURI)
assert.NotEmpty(t, res.ExpiresIn)
})
}

func withIDToken(t *testing.T, extras map[string]interface{}) (*Authentication, error) {
t.Helper()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
version: 2
interactions:
- id: 0
request:
proto: HTTP/1.1
proto_major: 1
proto_minor: 1
content_length: 194
transfer_encoding: []
trailer: {}
host: go-auth0-dev.eu.auth0.com
remote_addr: ""
request_uri: ""
body: client_id=test-client_id&client_secret=test-client_secret&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_type=code
form:
client_id:
- test-client_id
client_secret:
- test-client_secret
redirect_uri:
- http://localhost:3000/callback
response_type:
- code
headers:
Content-Type:
- application/x-www-form-urlencoded
url: https://go-auth0-dev.eu.auth0.com/oauth/par
method: POST
response:
proto: HTTP/2.0
proto_major: 2
proto_minor: 0
transfer_encoding: []
trailer: {}
content_length: 132
uncompressed: false
body: '{"expires_in":30,"request_uri":"urn:ietf:params:oauth:request_uri:test-value"}'
headers:
Content-Type:
- application/json; charset=utf-8
status: 201 Created
code: 201
duration: 669.090416ms
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
version: 2
interactions:
- id: 0
request:
proto: HTTP/1.1
proto_major: 1
proto_minor: 1
content_length: 395
transfer_encoding: []
trailer: {}
host: go-auth0-dev.eu.auth0.com
remote_addr: ""
request_uri: ""
body: audience=test-audience&client_id=test-client_id&client_secret=test-client_secret&code_challenge=n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg&connection=Username-Password&invitation=invite&nonce=abc123&organization=my-org&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_mode=form_post&response_type=code&scope=openid+profile+email&test=value
form:
audience:
- test-audience
client_id:
- test-client_id
client_secret:
- test-client_secret
code_challenge:
- n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg
connection:
- Username-Password
invitation:
- invite
nonce:
- abc123
organization:
- my-org
redirect_uri:
- http://localhost:3000/callback
response_mode:
- form_post
response_type:
- code
scope:
- openid profile email
test:
- value
headers:
Content-Type:
- application/x-www-form-urlencoded
url: https://go-auth0-dev.eu.auth0.com/oauth/par
method: POST
response:
proto: HTTP/2.0
proto_major: 2
proto_minor: 0
transfer_encoding: []
trailer: {}
content_length: 132
uncompressed: false
body: '{"expires_in":30,"request_uri":"urn:ietf:params:oauth:request_uri:test-value"}'
headers:
Content-Type:
- application/json; charset=utf-8
status: 201 Created
code: 201
duration: 397.494667ms

0 comments on commit 93053ce

Please sign in to comment.