Skip to content

Commit 79dd1dd

Browse files
authored
Add private_key_jwt client authentication method (#6)
1 parent d7de143 commit 79dd1dd

File tree

7 files changed

+108
-46
lines changed

7 files changed

+108
-46
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,14 @@ oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
186186
--auth-method client_secret_jwt \
187187
--scopes introspect_tokens,revoke_tokens
188188
```
189+
190+
### Private Key JWT
191+
192+
``` sh
193+
oauth2c https://oauth2c.us.authz.cloudentity.io/oauth2c/demo \
194+
--client-id 582af0afb0d74554aa7af47849edb222 \
195+
--signing-key https://pastebin.com/raw/WMkzhjhm \
196+
--grant-type client_credentials \
197+
--auth-method private_key_jwt \
198+
--scopes introspect_tokens,revoke_tokens
199+
```

cmd/log.go

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package cmd
22

33
import (
4+
"bytes"
5+
"crypto/ecdsa"
6+
"crypto/rsa"
7+
"crypto/x509"
48
"encoding/json"
9+
"encoding/pem"
510
"strings"
611

712
"github.com/cloudentity/oauth2c/internal/oauth2"
@@ -111,38 +116,62 @@ func LogAuthMethod(config oauth2.ClientConfig) {
111116
}
112117
}
113118

114-
func LogAssertion(request oauth2.Request) {
115-
assertion := request.Form.Get("assertion")
119+
func LogAssertion(request oauth2.Request, title string, name string) {
120+
var (
121+
assertion = request.Form.Get(name)
122+
token *jwt.Token
123+
claims jwt.MapClaims
124+
err error
125+
)
116126

117-
if assertion != "" {
118-
var claims jwt.MapClaims
127+
if assertion == "" {
128+
return
129+
}
119130

120-
if token, _, err := parser.ParseUnverified(assertion, &claims); err != nil {
131+
if token, _, err = parser.ParseUnverified(assertion, &claims); err != nil {
132+
pterm.Error.Println(err)
133+
return
134+
}
135+
136+
pterm.DefaultBox.WithTitle(title).Printfln("%s = JWT-%s(payload)", name, token.Header["alg"])
137+
pterm.Println("")
138+
pterm.Println("Payload")
139+
LogJson(claims)
140+
pterm.Println("")
141+
142+
pterm.Println("Key")
143+
switch key := request.Key.(type) {
144+
case *rsa.PrivateKey:
145+
p := bytes.Buffer{}
146+
147+
if err = pem.Encode(&p, &pem.Block{
148+
Type: "RSA PRIVATE KEY",
149+
Bytes: x509.MarshalPKCS1PrivateKey(key),
150+
}); err != nil {
121151
pterm.Error.Println(err)
122-
} else {
123-
pterm.DefaultBox.WithTitle("JWT Bearer assertion").Printfln("assertion = JWT-%s(payload)", token.Header["alg"])
124-
pterm.Println("")
125-
pterm.Println("Payload")
126-
LogJson(claims)
127-
pterm.Println("")
128152
}
129-
}
130-
}
131153

132-
func LogClientAssertion(request oauth2.Request) {
133-
assertion := request.Form.Get("client_assertion")
154+
pterm.FgGray.Printfln(p.String())
155+
case *ecdsa.PrivateKey:
156+
b, err := x509.MarshalECPrivateKey(key)
157+
158+
if err != nil {
159+
pterm.Error.Println(err)
160+
}
134161

135-
if assertion != "" {
136-
var claims jwt.MapClaims
162+
p := bytes.Buffer{}
137163

138-
if _, _, err := parser.ParseUnverified(assertion, &claims); err != nil {
164+
if err = pem.Encode(&p, &pem.Block{
165+
Type: "EC PRIVATE KEY",
166+
Bytes: b,
167+
}); err != nil {
139168
pterm.Error.Println(err)
140-
} else {
141-
pterm.DefaultBox.WithTitle("client_assertion").Printfln("client_assertion = JWT-HS256(payload)")
142-
pterm.Println("")
143-
pterm.Println("Payload")
144-
LogJson(claims)
145-
pterm.Println("")
146169
}
170+
171+
pterm.FgGray.Printfln(p.String())
172+
case []byte:
173+
pterm.FgGray.Println(string(key))
147174
}
175+
176+
pterm.Println("")
148177
}

cmd/oauth2.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,8 +399,8 @@ func tokenEndpointFlow(
399399
return err
400400
}
401401

402-
LogAssertion(tokenRequest)
403-
LogClientAssertion(tokenRequest)
402+
LogAssertion(tokenRequest, "Assertion", "assertion")
403+
LogAssertion(tokenRequest, "Client assertion", "client_assertion")
404404
LogAuthMethod(clientConfig)
405405
LogRequestAndResponse(tokenRequest, tokenResponse)
406406
LogTokenPayloadln(tokenResponse)

internal/oauth2/oauth2.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const (
3737
ClientSecretBasicAuthMethod string = "client_secret_basic"
3838
ClientSecretPostAuthMethod string = "client_secret_post"
3939
ClientSecretJwtAuthMethod string = "client_secret_jwt"
40-
// PrivateKeyJwtAuthMethod string = "private_key_jwt"
40+
PrivateKeyJwtAuthMethod string = "private_key_jwt"
4141
// SelfSignedTLSAuthMethod string = "self_signed_tls_client_auth"
4242
// TLSClientAuthMethod string = "tls_client_auth"
4343
// NoneAuthMethod string = "none"
@@ -261,7 +261,7 @@ func RequestToken(
261261
case JWTBearerGrantType:
262262
var assertion string
263263

264-
if assertion, err = SignJWT(
264+
if assertion, request.Key, err = SignJWT(
265265
AssertionClaims(sconfig, cconfig),
266266
JWKSigner(cconfig, hc),
267267
); err != nil {
@@ -278,13 +278,25 @@ func RequestToken(
278278
case ClientSecretJwtAuthMethod:
279279
var clientAssertion string
280280

281-
if clientAssertion, err = SignJWT(
281+
if clientAssertion, request.Key, err = SignJWT(
282282
ClientAssertionClaims(sconfig, cconfig),
283283
SecretSigner([]byte(cconfig.ClientSecret)),
284284
); err != nil {
285285
return request, response, err
286286
}
287287

288+
request.Form.Set("client_assertion_type", JwtBearerClientAssertion)
289+
request.Form.Set("client_assertion", clientAssertion)
290+
case PrivateKeyJwtAuthMethod:
291+
var clientAssertion string
292+
293+
if clientAssertion, request.Key, err = SignJWT(
294+
ClientAssertionClaims(sconfig, cconfig),
295+
JWKSigner(cconfig, hc),
296+
); err != nil {
297+
return request, response, err
298+
}
299+
288300
request.Form.Set("client_assertion_type", JwtBearerClientAssertion)
289301
request.Form.Set("client_assertion", clientAssertion)
290302
}

internal/oauth2/signing.go

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,35 +52,41 @@ func ReadKey(location string, hc *http.Client) (jose.JSONWebKey, error) {
5252
return keys.Keys[0], nil
5353
}
5454

55-
type SignerProvider func() (jose.Signer, error)
55+
type SignerProvider func() (jose.Signer, interface{}, error)
5656

5757
func JWKSigner(clientConfig ClientConfig, hc *http.Client) SignerProvider {
58-
return func() (signer jose.Signer, err error) {
58+
return func() (signer jose.Signer, _ interface{}, err error) {
5959
var key jose.JSONWebKey
6060

6161
if clientConfig.SigningKey == "" {
62-
return nil, errors.New("no signing key path")
62+
return nil, nil, errors.New("no signing key path")
6363
}
6464

6565
if key, err = ReadKey(clientConfig.SigningKey, hc); err != nil {
66-
return nil, errors.Wrapf(err, "failed to read signing key from %s", clientConfig.SigningKey)
66+
return nil, nil, errors.Wrapf(err, "failed to read signing key from %s", clientConfig.SigningKey)
6767
}
6868

69-
return jose.NewSigner(jose.SigningKey{
69+
if signer, err = jose.NewSigner(jose.SigningKey{
7070
Algorithm: jose.SignatureAlgorithm(key.Algorithm),
7171
Key: key.Key,
7272
}, &jose.SignerOptions{
7373
ExtraHeaders: map[jose.HeaderKey]interface{}{"kid": key.KeyID},
74-
})
74+
}); err != nil {
75+
return nil, nil, errors.Wrapf(err, "failed to create a signer")
76+
}
77+
78+
return signer, key.Key, nil
7579
}
7680
}
7781

7882
func SecretSigner(secret []byte) SignerProvider {
79-
return func() (jose.Signer, error) {
80-
return jose.NewSigner(jose.SigningKey{
83+
return func() (jose.Signer, interface{}, error) {
84+
signer, err := jose.NewSigner(jose.SigningKey{
8185
Algorithm: jose.HS256,
8286
Key: secret,
8387
}, nil)
88+
89+
return signer, secret, err
8490
}
8591
}
8692

@@ -123,30 +129,33 @@ func ClientAssertionClaims(serverConfig ServerConfig, clientConfig ClientConfig)
123129
}
124130
}
125131

126-
func SignJWT(claimsProvider ClaimsProvider, signerProvider SignerProvider) (string, error) {
132+
func SignJWT(claimsProvider ClaimsProvider, signerProvider SignerProvider) (jwt string, key interface{}, err error) {
127133
var (
128134
signer jose.Signer
129135
claims map[string]interface{}
130136
jws *jose.JSONWebSignature
131137
bs []byte
132-
err error
133138
)
134139

135-
if signer, err = signerProvider(); err != nil {
136-
return "", errors.Wrapf(err, "failed to create signer")
140+
if signer, key, err = signerProvider(); err != nil {
141+
return "", nil, errors.Wrapf(err, "failed to create signer")
137142
}
138143

139144
if claims, err = claimsProvider(); err != nil {
140-
return "", errors.Wrapf(err, "failed to build claims")
145+
return "", nil, errors.Wrapf(err, "failed to build claims")
141146
}
142147

143148
if bs, err = json.Marshal(claims); err != nil {
144-
return "", errors.Wrapf(err, "failed to serialize claims")
149+
return "", nil, errors.Wrapf(err, "failed to serialize claims")
145150
}
146151

147152
if jws, err = signer.Sign(bs); err != nil {
148-
return "", errors.Wrapf(err, "failed to sign jwt")
153+
return "", nil, errors.Wrapf(err, "failed to sign jwt")
154+
}
155+
156+
if jwt, err = jws.CompactSerialize(); err != nil {
157+
return "", nil, err
149158
}
150159

151-
return jws.CompactSerialize()
160+
return jwt, key, nil
152161
}

internal/oauth2/signing_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func TestSignJWT(t *testing.T) {
3232
},
3333
)
3434

35-
jwt, err := oauth2.SignJWT(claims, oauth2.JWKSigner(oauth2.ClientConfig{
35+
jwt, _, err := oauth2.SignJWT(claims, oauth2.JWKSigner(oauth2.ClientConfig{
3636
SigningKey: "./testdata/jwks-private.json",
3737
}, http.DefaultClient))
3838
require.NoError(t, err)

internal/oauth2/tracing.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ type Request struct {
77
URL *url.URL
88
Headers map[string][]string
99
Form url.Values
10+
Key interface{}
1011
}
1112

1213
func (r *Request) Get(key string) string {

0 commit comments

Comments
 (0)