-
Notifications
You must be signed in to change notification settings - Fork 14
/
oidc_client.go
249 lines (213 loc) · 7.87 KB
/
oidc_client.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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
package oauth2
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/golang-jwt/jwt/v4"
"github.com/hashicorp/hcl/v2"
acjwt "github.com/avenga/couper/accesscontrol/jwt"
"github.com/avenga/couper/errors"
"github.com/avenga/couper/oauth2/oidc"
)
// OidcClient represents an OpenID Connect client using the authorization code flow.
type OidcClient struct {
*AuthCodeClient
config *oidc.Config
backends map[string]http.RoundTripper
jwtParser *jwt.Parser
}
// NewOidcClient creates a new OIDC client.
func NewOidcClient(evalCtx *hcl.EvalContext, oidcConfig *oidc.Config) (*OidcClient, error) {
var algorithms []string
for _, a := range append(acjwt.RSAAlgorithms, acjwt.ECDSAlgorithms...) {
algorithms = append(algorithms, a.String())
}
options := []jwt.ParserOption{
jwt.WithValidMethods(algorithms),
// no equivalent in new lib
// jwt.WithLeeway(time.Second),
}
o := &OidcClient{
config: oidcConfig,
backends: oidcConfig.Backends(),
jwtParser: jwt.NewParser(options...),
}
acClient, err := NewAuthCodeClient(evalCtx, oidcConfig, oidcConfig, o.backends["token_backend"])
if err != nil {
return nil, err
}
o.AuthCodeClient = acClient
o.AuthCodeFlowClient = o
return o, nil
}
// validateTokenResponseData validates the token response data
func (o *OidcClient) validateTokenResponseData(ctx context.Context, tokenResponseData map[string]interface{}, hashedVerifierValue, verifierValue, accessToken string) error {
if idTokenString, ok := tokenResponseData["id_token"].(string); ok {
idTokenClaims := jwt.MapClaims{}
_, err := o.jwtParser.ParseWithClaims(idTokenString, idTokenClaims, o.Keyfunc)
if err != nil {
return err
}
var userinfo map[string]interface{}
userinfo, err = o.validateIDTokenClaims(ctx, idTokenClaims, hashedVerifierValue, verifierValue, accessToken)
if err != nil {
return err
}
// treat token claims as map for context
tokenResponseData["id_token_claims"] = map[string]interface{}(idTokenClaims)
tokenResponseData["userinfo"] = userinfo
return nil
}
return errors.Oauth2.Message("missing id_token in token response")
}
func (o *OidcClient) Keyfunc(token *jwt.Token) (interface{}, error) {
return o.config.JWKS().
GetSigKeyForToken(token)
}
func (o *OidcClient) validateIDTokenClaims(ctx context.Context, idTokenClaims jwt.MapClaims, hashedVerifierValue, verifierValue string, accessToken string) (map[string]interface{}, error) {
// 2. ID Token
// iss
// REQUIRED.
// handled by VerifyIssuer(issuer, true)
// sub
// REQUIRED.
var subIdtoken string
if s, ok := idTokenClaims["sub"].(string); ok {
subIdtoken = s
} else {
return nil, errors.Oauth2.Messagef("missing sub claim in ID token")
}
// aud
// REQUIRED.
// handled by VerifyAudience(issuer, true)
// exp
// REQUIRED.
if _, expExists := idTokenClaims["exp"]; !expExists {
return nil, errors.Oauth2.Message("missing exp claim in ID token")
}
// iat
// REQUIRED.
_, iatExists := idTokenClaims["iat"]
if !iatExists {
return nil, errors.Oauth2.Message("missing iat claim in ID token")
}
// 3.1.3.7. ID Token Validation
// 2. The Issuer Identifier for the OpenID Provider (which is typically
// obtained during Discovery) MUST exactly match the value of the
// iss (issuer) Claim.
issuer, err := o.config.GetIssuer()
if err != nil {
return nil, errors.Oauth2.With(err)
}
if !idTokenClaims.VerifyIssuer(issuer, true) {
return nil, errors.Oauth2.Message("invalid issuer in ID token")
}
// 3. The Client MUST validate that the aud (audience) Claim contains
// its client_id value registered at the Issuer identified by the
// iss (issuer) Claim as an audience. The aud (audience) Claim MAY
// contain an array with more than one element. The ID Token MUST
// be rejected if the ID Token does not list the Client as a valid
// audience, or if it contains additional audiences not trusted by
// the Client.
if !idTokenClaims.VerifyAudience(o.config.GetClientID(), true) {
return nil, errors.Oauth2.Message("invalid audience in ID token")
}
// 4. If the ID Token contains multiple audiences, the Client SHOULD verify
// that an azp Claim is present.
azp, azpExists := idTokenClaims["azp"]
if auds, audsOK := idTokenClaims["aud"].([]interface{}); audsOK && len(auds) > 1 && !azpExists {
return nil, errors.Oauth2.Message("missing azp claim in ID token")
}
// 5. If an azp (authorized party) Claim is present, the Client SHOULD
// verify that its client_id is the Claim Value.
if azpExists && azp != o.clientConfig.GetClientID() {
return nil, errors.Oauth2.Messagef("azp claim / client ID mismatch, azp = %q, client ID = %q", azp, o.clientConfig.GetClientID())
}
verifierMethod, err := o.config.GetVerifierMethod()
if err != nil {
return nil, err
}
// validate nonce claim value against CSRF token
if verifierMethod == "nonce" {
// 11. If a nonce value was sent in the Authentication Request, a nonce
// Claim MUST be present and its value checked to verify that it is the
// same value as the one that was sent in the Authentication Request.
// The Client SHOULD check the nonce value for replay attacks. The
// precise method for detecting replay attacks is Client specific.
var nonce string
if n, ok := idTokenClaims["nonce"].(string); ok {
nonce = n
} else {
return nil, errors.Oauth2.Message("missing nonce claim in ID token")
}
if hashedVerifierValue != nonce {
return nil, errors.Oauth2.Messagef("nonce mismatch: %q (from nonce claim) vs. %q (verifier_value: %q)", nonce, hashedVerifierValue, verifierValue)
}
}
userinfoData, subUserinfo, err := o.getUserinfo(ctx, accessToken)
if err != nil {
return nil, errors.Oauth2.Message("userinfo request error").With(err)
}
// 5.3.2. Successful UserInfo Response
// The sub Claim in the UserInfo Response MUST be verified to exactly
// match the sub Claim in the ID Token; if they do not match, the
// UserInfo Response values MUST NOT be used.
if subIdtoken != subUserinfo {
return nil, errors.Oauth2.Messagef("subject mismatch, in ID token %q, in userinfo response %q", subIdtoken, subUserinfo)
}
return userinfoData, nil
}
func (o *OidcClient) getUserinfo(ctx context.Context, accessToken string) (map[string]interface{}, string, error) {
userinfoReq, err := o.newUserinfoRequest(ctx, accessToken)
if err != nil {
return nil, "", err
}
userinfoResponse, err := o.requestUserinfo(userinfoReq)
if err != nil {
return nil, "", err
}
return parseUserinfoResponse(userinfoResponse)
}
func (o *OidcClient) requestUserinfo(userinfoReq *http.Request) ([]byte, error) {
ctx, cancel := context.WithCancel(userinfoReq.Context())
defer cancel()
userinfoRes, err := o.backends["userinfo_backend"].RoundTrip(userinfoReq.WithContext(ctx))
if err != nil {
return nil, err
}
defer userinfoRes.Body.Close()
userinfoResBytes, err := io.ReadAll(userinfoRes.Body)
if err != nil {
return nil, err
}
if userinfoRes.StatusCode != http.StatusOK {
return nil, fmt.Errorf("wrong status code, status=%d, response=%q", userinfoRes.StatusCode, string(userinfoResBytes))
}
return userinfoResBytes, nil
}
func parseUserinfoResponse(userinfoResponse []byte) (map[string]interface{}, string, error) {
var userinfoData map[string]interface{}
err := json.Unmarshal(userinfoResponse, &userinfoData)
if err != nil {
return nil, "", err
}
sub, ok := userinfoData["sub"].(string)
if !ok {
return nil, "", fmt.Errorf("missing sub property, response=%q", string(userinfoResponse))
}
return userinfoData, sub, nil
}
func (o *OidcClient) newUserinfoRequest(ctx context.Context, accessToken string) (*http.Request, error) {
userinfoEndpoint, err := o.config.GetUserinfoEndpoint()
if err != nil {
return nil, err
}
outreq, err := http.NewRequest(http.MethodGet, userinfoEndpoint, nil)
if err != nil {
return nil, err
}
outreq.Header.Set("Authorization", "Bearer "+accessToken)
return outreq.WithContext(ctx), nil
}