Skip to content

Commit 035a58d

Browse files
authored
DPoP support (#52)
1 parent 03bb954 commit 035a58d

File tree

15 files changed

+193
-9
lines changed

15 files changed

+193
-9
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
126126
--scopes openid,email,offline_access \
127127
--tls-cert https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/cert.pem \
128128
--tls-key https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/key.pem \
129-
--signing-key https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/key.json \
129+
--signing-key https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/rsa/key.json \
130130
--encryption-key https://oauth2c.us.authz.cloudentity.io/oauth2c/demo/.well-known/jwks.json \
131131
--request-object \
132132
--pkce \

cmd/oauth2.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ func NewOAuth2Cmd() (cmd *OAuth2Cmd) {
7171
cmd.PersistentFlags().StringVar(&cconfig.TLSRootCA, "tls-root-ca", "", "path to tls root ca pem file")
7272
cmd.PersistentFlags().BoolVar(&cconfig.Insecure, "insecure", false, "allow insecure connections")
7373
cmd.PersistentFlags().BoolVarP(&silent, "silent", "s", false, "silent mode")
74+
cmd.PersistentFlags().BoolVar(&cconfig.DPoP, "dpop", false, "use DPoP")
7475

7576
return cmd
7677
}

cmd/oauth2_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const (
1717

1818
TLSCertURL = "https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/cert.pem"
1919
TLSKeyURL = "https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/key.pem"
20-
SigningKeyURL = "https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/key.json"
20+
SigningKeyURL = "https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/rsa/key.json"
2121
)
2222

2323
type CommandTestCase struct {

data/ps/key.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"keys": [
3+
{
4+
"p": "_31C2N6keDMiqvs3eKZvKEAagzSm6aVLdZ_RwHYPGV_DoM9e4klZfqMdsHKvEuMYUsvS7oEsAy0OpMX81fyg95VkVzhM5OrriTiVsB2IZD85Trtkl8UAxR1whOGpWwPJIgiQ0dytUH3jT20TwX46Zd_TNuZiMvIMON2dDWHoXBU",
5+
"kty": "RSA",
6+
"q": "lqvkl1R_E0BTl-YoQPFts3JT4k4Xv2PC9-IvTt1LQM6chmJA_AG8zIki2dzbU-bCrHynUe-oJFvKO0M2mGtmXoCctqlIvrkK5qEmynjzAV4Rhf4jD9e3lAF-dsN7YlapS0hjLtVdPj14XjoPBkeur_eUoIK5WdmK_KeZ7FswvkM",
7+
"d": "TggdN4xl32oJJN1d8kHz7wZFvsLNau2tUsW_rKUw5pnGsXbPN8v0G4oDW67YwjvKES7wt6yY4wivDKFy58Y6pcrWE91jWVKyDMEI5h0dLL-tnggEYMCCd4NCBQ0pAcXE0RCl_7X4KEV5ABJtvyGprk1nJhFyxMgDlCzjwd7wdoQOmEceBfoBRhFnJPrjklOxpFeGY4cwny6XYf49kKWB5UB75CJn1ZpYHDsIWrhg1eE64tEJeXkePO0gZw_L5WAY15e8Mh7o7n9563wAEnNaP6PWZ2B4O-dGe7Sey6XFlKkGYDzoZaDCXuABtjA5_MMYjgWcfc2v9bC4tXlUdswO6Q",
8+
"e": "AQAB",
9+
"use": "sig",
10+
"kid": "rQUy3LYhOceyA3AcZbho4vnXIfaJjgVo_iPEwffvQ8M",
11+
"qi": "uN1F1YD55RohP8fu9Ut2JiaUkefrNbPqOr50sleWPZU-h70kvJr5B6hy7Ekk1-FPyJ7AU3b4qBPfUyoQ0--HokfssOTKXU8ZEYV47u507IMmCr6-NsoSAjQIrUUw01dM3BtwxufX-VWda1dbtVcDqqZ5DPPXabvPqh6QVfdhVSc",
12+
"dp": "PUVoE6SJYv44cTLgIcIgZFHDSfYFlYD7sNDMN9DYXCh4PQeeZLxchx9NTnSigfAOdETHaEV4LabPnTqSISt92wJr1vL8leW06Oq2E09x10DGWJheTnuDbMJbqrKHr_kfclcFjB7VPbmDGxg4pa3FCYt9Fux3Xmpn_fc_4-a4F-U",
13+
"alg": "PS256",
14+
"dq": "P2Z7XPZQNpCV3FAb1iABMkZEZ_DGa2GWM-p4T64ssUt_b8i-YYx1nneCM7yMigSLHDujyIWY8huxwDgrK_3daJyj1PTsyFxi6uMayI4WaxfjNcfXhx4VgHEUfvMI4ztmJ2iBW76qartBAB1cHx9gsWjzoIsBZX51zpTT3zIME7M",
15+
"n": "ll7x-VAQlsnf4Td5xKsKGgQtDUZj5uG4Fh-mDODe5n6iz_a7Of4WWkUMWacx4JWtbnjzJ8MfWVMcYXgBky1XQB9IcQ7M7otT60cULLKqXAlhxX6AWSlTwfyvNeXOFc4kjzZwU6yvfv4HpPlhzOob3cImjSx4hAWSsDC3igzzHdODFrt3nW8KApQObqQpMFcSuQLu3SshrC9GLnRYs7gxcs9qvfXcgHkPCmcCgbV4VjwbTtw3OyPeHMlJ0AoHt_ZXS6gIiwzUKlCp6MKn0OIH8XMSp4foYYNlSPW7OE0iLca8AJew9DsjtjhATbO-pldTpmNpYqpdrS1v0G3Tzxevfw"
16+
}
17+
]
18+
}

data/ps/public.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"keys": [
3+
{
4+
"kty": "RSA",
5+
"e": "AQAB",
6+
"use": "sig",
7+
"kid": "rQUy3LYhOceyA3AcZbho4vnXIfaJjgVo_iPEwffvQ8M",
8+
"alg": "PS256",
9+
"n": "ll7x-VAQlsnf4Td5xKsKGgQtDUZj5uG4Fh-mDODe5n6iz_a7Of4WWkUMWacx4JWtbnjzJ8MfWVMcYXgBky1XQB9IcQ7M7otT60cULLKqXAlhxX6AWSlTwfyvNeXOFc4kjzZwU6yvfv4HpPlhzOob3cImjSx4hAWSsDC3igzzHdODFrt3nW8KApQObqQpMFcSuQLu3SshrC9GLnRYs7gxcs9qvfXcgHkPCmcCgbV4VjwbTtw3OyPeHMlJ0AoHt_ZXS6gIiwzUKlCp6MKn0OIH8XMSp4foYYNlSPW7OE0iLca8AJew9DsjtjhATbO-pldTpmNpYqpdrS1v0G3Tzxevfw"
10+
}
11+
]
12+
}
File renamed without changes.
File renamed without changes.

docs/examples.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
199199
--grant-type urn:ietf:params:oauth:grant-type:jwt-bearer \
200200
--auth-method client_secret_basic \
201201
--scopes email \
202-
--signing-key https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/key.json \
202+
--signing-key https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/rsa/key.json \
203203
--assertion '{"sub":"[email protected]"}'
204204
```
205205
</details>
@@ -337,7 +337,7 @@ JWT methods using a client secret, as the private key is never shared with the O
337337
``` sh
338338
oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
339339
--client-id 582af0afb0d74554aa7af47849edb222 \
340-
--signing-key https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/key.json \
340+
--signing-key https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/rsa/key.json \
341341
--grant-type client_credentials \
342342
--auth-method private_key_jwt \
343343
--scopes introspect_tokens,revoke_tokens
@@ -483,7 +483,7 @@ oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
483483
--grant-type authorization_code \
484484
--auth-method client_secret_post \
485485
--scopes openid,email,offline_access \
486-
--encryption-key https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/key.json
486+
--encryption-key https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/rsa/key.json
487487
```
488488
</details>
489489

@@ -511,3 +511,26 @@ oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
511511
</details>
512512

513513
[Learn more about PAR](https://cloudentity.com/developers/basics/oauth-grant-types/pushed-authorization-requests/)
514+
515+
### DPoP
516+
517+
DPoP, or Demonstration of Proof of Possession, is an extension that describes a technique to cryptographically bind access
518+
tokens to a particular client when they are issued. This is one of many attempts at improving the security of Bearer Tokens
519+
by requiring the application using the token to authenticate itself.
520+
521+
<details>
522+
<summary>Show example</summary>
523+
524+
``` sh
525+
oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
526+
--client-id cauktionbud6q8ftlqq0 \
527+
--client-secret HCwQ5uuUWBRHd04ivjX5Kl0Rz8zxMOekeLtqzki0GPc \
528+
--response-types code \
529+
--response-mode query \
530+
--grant-type authorization_code \
531+
--auth-method client_secret_basic \
532+
--scopes openid,email,offline_access \
533+
--signing-key https://raw.githubusercontent.com/cloudentity/oauth2c/master/data/ps/key.json \
534+
--dpop
535+
```
536+
</details>

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.19
55
require (
66
github.com/go-jose/go-jose/v3 v3.0.0
77
github.com/golang-jwt/jwt/v4 v4.4.3
8+
github.com/google/uuid v1.3.0
89
github.com/grantae/certinfo v0.0.0-20170412194111-59d56a35515b
910
github.com/hashicorp/go-multierror v1.1.1
1011
github.com/imdario/mergo v0.3.13
@@ -23,7 +24,6 @@ require (
2324
atomicgo.dev/keyboard v0.2.8 // indirect
2425
github.com/containerd/console v1.0.3 // indirect
2526
github.com/davecgh/go-spew v1.1.1 // indirect
26-
github.com/google/uuid v1.3.0 // indirect
2727
github.com/gookit/color v1.5.0 // indirect
2828
github.com/hashicorp/errwrap v1.0.0 // indirect
2929
github.com/inconshreveable/mousetrap v1.0.0 // indirect

internal/oauth2/dpop.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package oauth2
2+
3+
import (
4+
"crypto"
5+
"encoding/base64"
6+
"encoding/json"
7+
"net/http"
8+
"time"
9+
10+
"github.com/go-jose/go-jose/v3"
11+
"github.com/google/uuid"
12+
"github.com/pkg/errors"
13+
)
14+
15+
const (
16+
DPoPHeaderName = "DPoP"
17+
DPoPHeaderType = "dpop+jwt"
18+
)
19+
20+
type DPoPClaims struct {
21+
Htm string `json:"htm"`
22+
Htu string `json:"htu"`
23+
Jti string `json:"jti"`
24+
IssuedAt int64 `json:"iat"`
25+
}
26+
27+
func DPoPSignRequest(signingKey string, hc *http.Client, r *http.Request) error {
28+
var (
29+
key jose.JSONWebKey
30+
proof string
31+
signer jose.Signer
32+
bytes []byte
33+
signature *jose.JSONWebSignature
34+
err error
35+
)
36+
37+
if key, err = ReadKey(SigningKey, signingKey, hc); err != nil {
38+
return errors.Wrapf(err, "failed to read signing key from %s", signingKey)
39+
}
40+
41+
if key.Algorithm == "" {
42+
return errors.New("signing key algorithm must be set")
43+
}
44+
45+
if key.IsPublic() {
46+
return errors.New("signing key must be private")
47+
}
48+
49+
if !key.Valid() {
50+
return errors.New("signing key is not valid")
51+
}
52+
53+
sig := jose.SigningKey{
54+
Algorithm: jose.SignatureAlgorithm(key.Algorithm),
55+
Key: key.Key,
56+
}
57+
58+
opts := &jose.SignerOptions{
59+
ExtraHeaders: map[jose.HeaderKey]interface{}{
60+
jose.HeaderType: DPoPHeaderType,
61+
},
62+
EmbedJWK: true,
63+
}
64+
65+
if signer, err = jose.NewSigner(sig, opts); err != nil {
66+
return errors.Wrapf(err, "failed to create signer")
67+
}
68+
69+
claims := DPoPClaims{
70+
Htm: r.Method,
71+
Htu: r.URL.String(),
72+
Jti: uuid.New().String(),
73+
IssuedAt: time.Now().Unix(),
74+
}
75+
76+
if bytes, err = json.Marshal(claims); err != nil {
77+
return err
78+
}
79+
80+
if signature, err = signer.Sign(bytes); err != nil {
81+
return err
82+
}
83+
84+
if proof, err = signature.CompactSerialize(); err != nil {
85+
return err
86+
}
87+
88+
r.Header.Set(DPoPHeaderName, proof)
89+
90+
return nil
91+
}
92+
93+
func DPoPThumbprint(signingKey string, hc *http.Client) (string, error) {
94+
var (
95+
key jose.JSONWebKey
96+
thumbprint []byte
97+
err error
98+
)
99+
100+
if key, err = ReadKey(SigningKey, signingKey, hc); err != nil {
101+
return "", errors.Wrapf(err, "failed to read signing key from %s", signingKey)
102+
}
103+
104+
public := key.Public()
105+
106+
if thumbprint, err = public.Thumbprint(crypto.SHA256); err != nil {
107+
return "", err
108+
}
109+
110+
return base64.RawURLEncoding.EncodeToString(thumbprint), nil
111+
}

0 commit comments

Comments
 (0)