Skip to content

Commit

Permalink
[SDK-4738] Add support for performing Pushed Authorization Requests (#…
Browse files Browse the repository at this point in the history
…327)

Co-authored-by: Rita Zerrizuela <[email protected]>
  • Loading branch information
ewanharris and Widcket authored Dec 11, 2023
1 parent 6faf79f commit 9fef632
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 0 deletions.
60 changes: 60 additions & 0 deletions authentication/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/url"
"strings"
"time"

"github.com/google/uuid"
Expand Down Expand Up @@ -207,6 +208,53 @@ 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) {
missing := []string{}
check(&missing, "ClientID", (body.ClientID != "" || o.authentication.clientID != ""))
check(&missing, "ResponseType", body.ResponseType != "")
check(&missing, "RedirectURI", body.RedirectURI != "")

if len(missing) > 0 {
return nil, fmt.Errorf("Missing required fields: %s", strings.Join(missing, ", "))
}

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 +337,15 @@ func createClientAssertion(clientAssertionSigningAlg, clientAssertionSigningKey,

return string(b), nil
}

func addIfNotEmpty(key string, value string, qs url.Values) {
if value != "" {
qs.Set(key, value)
}
}

func check(errors *[]string, key string, c bool) {
if !c {
*errors = append(*errors, key)
}
}
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 from a Pushed Authorization Request.
type PushedAuthorizationRequestResponse struct {
RequestURI string `json:"request_uri,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"`
}
65 changes: 65 additions & 0 deletions authentication/oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,71 @@ 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{
ResponseType: "code",
RedirectURI: "http://localhost:3000/callback",
})
assert.ErrorContains(t, err, "client_secret or client_assertion is required but not provided")
})

t.Run("Should require a ClientID, ResponseType and RedirectURI", func(t *testing.T) {
auth, err := New(
context.Background(),
domain,
)
require.NoError(t, err)
_, err = auth.OAuth.PushedAuthorization(context.Background(), oauth.PushedAuthorizationRequest{})
assert.ErrorContains(t, err, "Missing required fields: ClientID, ResponseType, RedirectURI")
})

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 9fef632

Please sign in to comment.