Skip to content

Commit f58a84f

Browse files
authored
PAR (#33)
1 parent a0d2e52 commit f58a84f

File tree

8 files changed

+228
-87
lines changed

8 files changed

+228
-87
lines changed

README.md

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ The available flags are:
6666
--auth-method string token endpoint authentication method
6767
--client-id string client identifier
6868
--client-secret string client secret
69+
--encryption-key string path or url to encryption key in jwks format
6970
--grant-type string grant type
7071
-h, --help help for oauthc
7172
--insecure allow insecure connections
72-
--no-pkce disable proof key for code exchange (PKCE)
73+
--par enable pushed authorization requests (PAR)
7374
--password string resource owner password credentials grant flow password
7475
--pkce enable proof key for code exchange (PKCE)
7576
--refresh-token string refresh token
@@ -114,8 +115,7 @@ oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
114115
--response-mode query \
115116
--grant-type authorization_code \
116117
--auth-method client_secret_basic \
117-
--scopes openid,email,offline_access \
118-
--no-pkce
118+
--scopes openid,email,offline_access
119119
```
120120

121121
[Learn more about authorization code flow](https://cloudentity.com/developers/basics/oauth-grant-types/authorization-code-flow/)
@@ -155,8 +155,7 @@ oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
155155
--response-mode form_post \
156156
--grant-type authorization_code \
157157
--auth-method client_secret_basic \
158-
--scopes openid,email,offline_access \
159-
--no-pkce
158+
--scopes openid,email,offline_access
160159
```
161160

162161
[Learn more about the hybrid flow](https://cloudentity.com/developers/basics/oauth-grant-types/hybrid-flow/)
@@ -204,7 +203,6 @@ oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
204203
> --grant-type authorization_code \
205204
> --auth-method client_secret_basic \
206205
> --scopes openid,email,offline_access \
207-
> --no-pkce \
208206
> --silent | jq -r .refresh_token`
209207
> ```
210208
@@ -297,7 +295,6 @@ oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
297295
> --grant-type authorization_code \
298296
> --auth-method client_secret_basic \
299297
> --scopes openid,email,offline_access \
300-
> --no-pkce \
301298
> --silent | jq -r .access_token`
302299
> ```
303300
@@ -466,8 +463,7 @@ oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
466463
--response-mode query.jwt \
467464
--grant-type authorization_code \
468465
--auth-method client_secret_basic \
469-
--scopes openid,email,offline_access \
470-
--no-pkce
466+
--scopes openid,email,offline_access
471467
```
472468

473469
#### Signed and encrypted JWT
@@ -481,10 +477,30 @@ oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
481477
--grant-type authorization_code \
482478
--auth-method client_secret_post \
483479
--scopes openid,email,offline_access \
484-
--encryption-key https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/key.json \
485-
--no-pkce
480+
--encryption-key https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/key.json
481+
```
482+
483+
#### PAR
484+
485+
Pushed Authorization Requests (PAR) is an extension of the OAuth 2.0 specification that enables client applications
486+
to push the payloads of authorization requests directly to the authorization server via a PAR endpoint.
487+
This allows for more efficient and secure handling of authorization requests. PAR can be useful for client applications
488+
that require a high level of security, and may be required for compliance with certain security profiles and regulations.
489+
490+
``` sh
491+
oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
492+
--client-id cauktionbud6q8ftlqq0 \
493+
--client-secret HCwQ5uuUWBRHd04ivjX5Kl0Rz8zxMOekeLtqzki0GPc \
494+
--response-types code \
495+
--response-mode query \
496+
--grant-type authorization_code \
497+
--auth-method client_secret_basic \
498+
--scopes openid,email,offline_access \
499+
--par
486500
```
487501

502+
[Learn more about PAR](https://cloudentity.com/developers/basics/oauth-grant-types/pushed-authorization-requests/)
503+
488504
## License
489505

490506
`oauth2c` is released under the [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0).

cmd/oauth2.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func NewOAuth2Cmd() (cmd *OAuth2Cmd) {
5656
cmd.PersistentFlags().StringVar(&cconfig.ResponseMode, "response-mode", "", "response mode")
5757
cmd.PersistentFlags().StringSliceVar(&cconfig.Scopes, "scopes", []string{}, "requested scopes")
5858
cmd.PersistentFlags().BoolVar(&cconfig.PKCE, "pkce", false, "enable proof key for code exchange (PKCE)")
59-
cmd.PersistentFlags().BoolVar(&cconfig.NoPKCE, "no-pkce", false, "disable proof key for code exchange (PKCE)")
59+
cmd.PersistentFlags().BoolVar(&cconfig.PAR, "par", false, "enable pushed authorization requests (PAR)")
6060
cmd.PersistentFlags().StringVar(&cconfig.Assertion, "assertion", "", "claims for jwt bearer assertion")
6161
cmd.PersistentFlags().StringVar(&cconfig.SigningKey, "signing-key", "", "path or url to signing key in jwks format")
6262
cmd.PersistentFlags().StringVar(&cconfig.EncryptionKey, "encryption-key", "", "path or url to encryption key in jwks format")

cmd/oauth2_authorize_code.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010

1111
func (c *OAuth2Cmd) AuthorizationCodeGrantFlow(clientConfig oauth2.ClientConfig, serverConfig oauth2.ServerConfig, hc *http.Client) error {
1212
var (
13+
parRequest oauth2.Request
14+
parResponse oauth2.PARResponse
1315
authorizeRequest oauth2.Request
1416
callbackRequest oauth2.Request
1517
tokenRequest oauth2.Request
@@ -20,14 +22,30 @@ func (c *OAuth2Cmd) AuthorizationCodeGrantFlow(clientConfig oauth2.ClientConfig,
2022

2123
LogHeader("Authorization Code Flow")
2224

23-
// authorize endpoint
24-
LogSection("Request authorization")
25+
if clientConfig.PAR {
26+
LogSection("Request PAR")
2527

26-
if authorizeRequest, codeVerifier, err = oauth2.RequestAuthorization(addr, clientConfig, serverConfig); err != nil {
27-
return err
28-
}
28+
if parRequest, parResponse, authorizeRequest, codeVerifier, err = oauth2.RequestPAR(context.Background(), addr, clientConfig, serverConfig, hc); err != nil {
29+
LogRequestAndResponseln(parRequest, err)
30+
return err
31+
}
32+
33+
LogAssertion(parRequest, "Client assertion", "client_assertion")
34+
LogAuthMethod(clientConfig)
35+
LogRequestAndResponse(parRequest, parResponse)
36+
37+
LogSection("Request authorization")
2938

30-
LogRequest(authorizeRequest)
39+
LogRequest(authorizeRequest)
40+
} else {
41+
LogSection("Request authorization")
42+
43+
if authorizeRequest, codeVerifier, err = oauth2.RequestAuthorization(addr, clientConfig, serverConfig); err != nil {
44+
return err
45+
}
46+
47+
LogRequest(authorizeRequest)
48+
}
3149

3250
if codeVerifier != "" {
3351
Logln()

cmd/oauth2_device.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func (c *OAuth2Cmd) DeviceGrantFlow(clientConfig oauth2.ClientConfig, serverConf
2525
LogSection("Request device authorization")
2626

2727
if authorizationRequest, authorizationResponse, err = oauth2.RequestDeviceAuthorization(context.Background(), clientConfig, serverConfig, hc); err != nil {
28+
LogRequestAndResponseln(tokenRequest, err)
2829
return err
2930
}
3031

cmd/oauth2_prompt.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,6 @@ func PromptForClientConfig(client oauth2.ClientConfig, server oauth2.ServerConfi
4040
}
4141
}
4242

43-
// pkce
44-
switch client.GrantType {
45-
case oauth2.AuthorizationCodeGrantType:
46-
if !client.PKCE && !client.NoPKCE {
47-
client.PKCE = PromptBool("PKCE")
48-
}
49-
}
50-
5143
if client.ClientID == "" {
5244
client.ClientID = PromptString("Client ID")
5345
}

internal/oauth2/oauth2.go

Lines changed: 109 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package oauth2
33
import (
44
"context"
55
"crypto/sha256"
6-
"crypto/x509"
76
"encoding/base64"
87
"encoding/json"
98
"fmt"
@@ -62,7 +61,7 @@ type ClientConfig struct {
6261
Scopes []string
6362
AuthMethod string
6463
PKCE bool
65-
NoPKCE bool
64+
PAR bool
6665
Insecure bool
6766
ResponseType []string
6867
ResponseMode string
@@ -81,12 +80,8 @@ type ClientConfig struct {
8180
TLSRootCA string
8281
}
8382

84-
func RequestAuthorization(addr string, cconfig ClientConfig, sconfig ServerConfig) (r Request, codeVerifier string, err error) {
85-
if r.URL, err = url.Parse(sconfig.AuthorizationEndpoint); err != nil {
86-
return r, "", errors.Wrapf(err, "failed to parse authorization endpoint")
87-
}
88-
89-
values := url.Values{
83+
func NewAuthorizationRequest(addr string, cconfig ClientConfig) (values url.Values, codeVerifier string, err error) {
84+
values = url.Values{
9085
"client_id": {cconfig.ClientID},
9186
"redirect_uri": {"http://" + addr + "/callback"},
9287
"state": {shortuuid.New()},
@@ -111,7 +106,7 @@ func RequestAuthorization(addr string, cconfig ClientConfig, sconfig ServerConfi
111106
hash := sha256.New()
112107

113108
if _, err = hash.Write([]byte(codeVerifier)); err != nil {
114-
return r, "", err
109+
return values, "", err
115110
}
116111

117112
codeChallenge := CodeChallengeEncoder.EncodeToString(hash.Sum([]byte{}))
@@ -120,12 +115,108 @@ func RequestAuthorization(addr string, cconfig ClientConfig, sconfig ServerConfi
120115
values.Set("code_challenge_method", "S256")
121116
}
122117

118+
return values, codeVerifier, nil
119+
}
120+
121+
func RequestAuthorization(addr string, cconfig ClientConfig, sconfig ServerConfig) (r Request, codeVerifier string, err error) {
122+
var values url.Values
123+
124+
if r.URL, err = url.Parse(sconfig.AuthorizationEndpoint); err != nil {
125+
return r, "", errors.Wrapf(err, "failed to parse authorization endpoint")
126+
}
127+
128+
if values, codeVerifier, err = NewAuthorizationRequest(addr, cconfig); err != nil {
129+
return r, "", errors.Wrapf(err, "failed to create authorization request")
130+
}
131+
123132
r.URL.RawQuery = values.Encode()
124133
r.Method = http.MethodGet
125134

126135
return r, codeVerifier, nil
127136
}
128137

138+
type PARResponse struct {
139+
RequestURI string `json:"request_uri"`
140+
ExpiresIn int64 `json:"expires_in"`
141+
}
142+
143+
func RequestPAR(
144+
ctx context.Context,
145+
addr string,
146+
cconfig ClientConfig,
147+
sconfig ServerConfig,
148+
hc *http.Client,
149+
) (parRequest Request, parResponse PARResponse, authorizeRequest Request, codeVerifier string, err error) {
150+
var (
151+
req *http.Request
152+
resp *http.Response
153+
endpoint string
154+
)
155+
156+
// push authorization request to /par
157+
if parRequest.Form, codeVerifier, err = NewAuthorizationRequest(addr, cconfig); err != nil {
158+
return parRequest, parResponse, authorizeRequest, "", errors.Wrapf(err, "failed to create authorization request")
159+
}
160+
161+
if endpoint, err = parRequest.AuthenticateClient(
162+
sconfig.PushedAuthorizationRequestEndpoint,
163+
sconfig.MTLsEndpointAliases.PushedAuthorizationRequestEndpoint,
164+
cconfig,
165+
sconfig,
166+
hc,
167+
); err != nil {
168+
return parRequest, parResponse, authorizeRequest, "", errors.Wrapf(err, "failed to create client authentication request")
169+
}
170+
171+
if req, err = http.NewRequestWithContext(
172+
ctx,
173+
http.MethodPost,
174+
endpoint,
175+
strings.NewReader(parRequest.Form.Encode()),
176+
); err != nil {
177+
return parRequest, parResponse, authorizeRequest, codeVerifier, err
178+
}
179+
180+
if cconfig.AuthMethod == ClientSecretBasicAuthMethod {
181+
req.SetBasicAuth(cconfig.ClientID, cconfig.ClientSecret)
182+
}
183+
184+
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
185+
186+
parRequest.Method = req.Method
187+
parRequest.Headers = req.Header
188+
parRequest.URL = req.URL
189+
190+
if resp, err = hc.Do(req); err != nil {
191+
return parRequest, parResponse, authorizeRequest, codeVerifier, err
192+
}
193+
194+
defer resp.Body.Close()
195+
196+
if resp.StatusCode != http.StatusCreated {
197+
return parRequest, parResponse, authorizeRequest, codeVerifier, ParseError(resp)
198+
}
199+
200+
if err = json.NewDecoder(resp.Body).Decode(&parResponse); err != nil {
201+
return parRequest, parResponse, authorizeRequest, codeVerifier, fmt.Errorf("failed to parse token response: %w", err)
202+
}
203+
204+
// build request to /authorize
205+
if authorizeRequest.URL, err = url.Parse(sconfig.AuthorizationEndpoint); err != nil {
206+
return parRequest, parResponse, authorizeRequest, codeVerifier, errors.Wrapf(err, "failed to create authorization request")
207+
}
208+
209+
values := url.Values{
210+
"client_id": {cconfig.ClientID},
211+
"request_uri": {parResponse.RequestURI},
212+
}
213+
214+
authorizeRequest.URL.RawQuery = values.Encode()
215+
authorizeRequest.Method = http.MethodGet
216+
217+
return parRequest, parResponse, authorizeRequest, codeVerifier, nil
218+
}
219+
129220
func WaitForCallback(clientConfig ClientConfig, serverConfig ServerConfig, addr string, hc *http.Client) (request Request, err error) {
130221
var (
131222
srv = http.Server{Addr: addr}
@@ -273,7 +364,7 @@ func RequestToken(
273364
req *http.Request
274365
resp *http.Response
275366
params RequestTokenParams
276-
endpoint = sconfig.TokenEndpoint
367+
endpoint string
277368
body []byte
278369
)
279370

@@ -319,45 +410,14 @@ func RequestToken(
319410
request.Form.Set("device_code", params.DeviceCode)
320411
}
321412

322-
switch cconfig.AuthMethod {
323-
case NoneAuthMethod:
324-
request.Form.Set("client_id", cconfig.ClientID)
325-
case ClientSecretPostAuthMethod:
326-
request.Form.Set("client_id", cconfig.ClientID)
327-
request.Form.Set("client_secret", cconfig.ClientSecret)
328-
case ClientSecretJwtAuthMethod:
329-
var clientAssertion string
330-
331-
if clientAssertion, request.Key, err = SignJWT(
332-
ClientAssertionClaims(sconfig, cconfig),
333-
SecretSigner([]byte(cconfig.ClientSecret)),
334-
); err != nil {
335-
return request, response, err
336-
}
337-
338-
request.Form.Set("client_assertion_type", JwtBearerClientAssertion)
339-
request.Form.Set("client_assertion", clientAssertion)
340-
case PrivateKeyJwtAuthMethod:
341-
var clientAssertion string
342-
343-
if clientAssertion, request.Key, err = SignJWT(
344-
ClientAssertionClaims(sconfig, cconfig),
345-
JWKSigner(cconfig, hc),
346-
); err != nil {
347-
return request, response, err
348-
}
349-
350-
request.Form.Set("client_assertion_type", JwtBearerClientAssertion)
351-
request.Form.Set("client_assertion", clientAssertion)
352-
case TLSClientAuthMethod, SelfSignedTLSAuthMethod:
353-
endpoint = sconfig.MTLsEndpointAliases.TokenEndpoint
354-
request.Form.Set("client_id", cconfig.ClientID)
355-
356-
if tr, ok := hc.Transport.(*http.Transport); ok {
357-
if len(tr.TLSClientConfig.Certificates) > 0 {
358-
request.Cert, _ = x509.ParseCertificate(tr.TLSClientConfig.Certificates[0].Certificate[0])
359-
}
360-
}
413+
if endpoint, err = request.AuthenticateClient(
414+
sconfig.TokenEndpoint,
415+
sconfig.MTLsEndpointAliases.TokenEndpoint,
416+
cconfig,
417+
sconfig,
418+
hc,
419+
); err != nil {
420+
return request, response, err
361421
}
362422

363423
if params.RedirectURL != "" {

0 commit comments

Comments
 (0)