-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathid_token.go
148 lines (135 loc) · 4.61 KB
/
id_token.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package oidc
import (
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/json"
"fmt"
"hash"
"github.com/go-jose/go-jose/v3"
)
// IDToken is an oidc id_token.
// See https://openid.net/specs/openid-connect-core-1_0.html#IDToken.
type IDToken string
// RedactedIDToken is the redacted string or json for an oidc id_token.
const RedactedIDToken = "[REDACTED: id_token]"
// String will redact the token.
func (t IDToken) String() string {
return RedactedIDToken
}
// MarshalJSON will redact the token.
func (t IDToken) MarshalJSON() ([]byte, error) {
return json.Marshal(RedactedIDToken)
}
// Claims retrieves the IDToken claims.
func (t IDToken) Claims(claims interface{}) error {
const op = "IDToken.Claims"
if len(t) == 0 {
return fmt.Errorf("%s: id_token is empty: %w", op, ErrInvalidParameter)
}
if claims == nil {
return fmt.Errorf("%s: claims interface is nil: %w", op, ErrNilParameter)
}
return UnmarshalClaims(string(t), claims)
}
// VerifyAccessToken verifies the at_hash claim of the id_token against the hash
// of the access_token.
//
// It will return true when it can verify the access_token. It will return false
// when it's unable to verify the access_token.
//
// It will return an error whenever it's possible to verify the access_token and
// the verification fails.
//
// Note: while we support signing id_tokens with EdDSA, unfortunately the
// access_token hash cannot be verified without knowing the key's curve. See:
// https://bitbucket.org/openid/connect/issues/1125
//
// For more info about verifying access_tokens returned during an OIDC flow see:
// https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
func (t IDToken) VerifyAccessToken(accessToken AccessToken) (bool, error) {
const op = "VerifyAccessToken"
canVerify, err := t.verifyHashClaim("at_hash", string(accessToken))
if err != nil {
return canVerify, fmt.Errorf("%s: %w", op, err)
}
return canVerify, nil
}
// VerifyAuthorizationCode verifies the c_hash claim of the id_token against the
// hash of the authorization code.
//
// It will return true when it can verify the authorization code. It will return
// false when it's unable to verify the authorization code.
//
// It will return an error whenever it's possible to verify the authorization
// code and the verification fails.
//
// Note: while we support signing id_tokens with EdDSA, unfortunately the
// authorization code hash cannot be verified without knowing the key's curve.
// See: https://bitbucket.org/openid/connect/issues/1125
//
// For more info about authorization code verification using the id_token's
// c_hash claim see:
// https://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken
func (t IDToken) VerifyAuthorizationCode(code string) (bool, error) {
const op = "VerifyAccessToken"
canVerify, err := t.verifyHashClaim("c_hash", code)
if err != nil {
return canVerify, fmt.Errorf("%s: %w", op, err)
}
return canVerify, nil
}
func (t IDToken) verifyHashClaim(claimName string, token string) (bool, error) {
const op = "verifyHashClaim"
var claims map[string]interface{}
if err := t.Claims(&claims); err != nil {
return false, fmt.Errorf("%s: %w", op, err)
}
tokenHash, ok := claims[claimName].(string)
if !ok {
return false, nil
}
jws, err := jose.ParseSigned(string(t))
if err != nil {
return false, fmt.Errorf("%s: malformed jwt (%v): %w", op, err, ErrMalformedToken)
}
switch len(jws.Signatures) {
case 0:
return false, fmt.Errorf("%s: id_token not signed: %w", op, ErrTokenNotSigned)
case 1:
default:
return false, fmt.Errorf("%s: multiple signatures on id_token not supported", op)
}
sig := jws.Signatures[0]
if _, ok := supportedAlgorithms[Alg(sig.Header.Algorithm)]; !ok {
return false, fmt.Errorf("%s: id_token signed with algorithm %q: %w", op, sig.Header.Algorithm, ErrUnsupportedAlg)
}
sigAlgorithm := Alg(sig.Header.Algorithm)
var h hash.Hash
switch sigAlgorithm {
case RS256, ES256, PS256:
h = sha256.New()
case RS384, ES384, PS384:
h = sha512.New384()
case RS512, ES512, PS512:
h = sha512.New()
case EdDSA:
return false, nil
default:
return false, fmt.Errorf("%s: unsupported signing algorithm %s: %w", op, sigAlgorithm, ErrUnsupportedAlg)
}
_, _ = h.Write([]byte(token)) // hash documents that Write will never return an error
sum := h.Sum(nil)[:h.Size()/2]
actual := base64.RawURLEncoding.EncodeToString(sum)
if actual != tokenHash {
switch claimName {
case "at_hash":
return false, fmt.Errorf("%s: %w", op, ErrInvalidAtHash)
case "c_hash":
return false, fmt.Errorf("%s: %w", op, ErrInvalidCodeHash)
}
}
return true, nil
}