/
OpenIdCredentials.java
197 lines (170 loc) · 7.26 KB
/
OpenIdCredentials.java
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
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.security.openid;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.FormRequestContent;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.ajax.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>The credentials of an user to be authenticated with OpenID Connect. This will contain
* the OpenID ID Token and the OAuth 2.0 Access Token.</p>
*
* <p>
* This is constructed with an authorization code from the authentication request. This authorization code
* is then exchanged using {@link #redeemAuthCode(OpenIdConfiguration)} for a response containing the ID Token and Access Token.
* The response is then validated against the {@link OpenIdConfiguration}.
* </p>
*/
public class OpenIdCredentials implements Serializable
{
private static final Logger LOG = LoggerFactory.getLogger(OpenIdCredentials.class);
private static final long serialVersionUID = 4766053233370044796L;
private final String redirectUri;
private String authCode;
private Map<String, Object> response;
private Map<String, Object> claims;
private boolean verified = false;
public OpenIdCredentials(Map<String, Object> claims)
{
this.redirectUri = null;
this.authCode = null;
this.claims = claims;
}
public OpenIdCredentials(String authCode, String redirectUri)
{
this.authCode = authCode;
this.redirectUri = redirectUri;
}
public String getUserId()
{
return (String)claims.get("sub");
}
public Map<String, Object> getClaims()
{
return claims;
}
public Map<String, Object> getResponse()
{
return response;
}
public void redeemAuthCode(OpenIdConfiguration configuration) throws Exception
{
if (LOG.isDebugEnabled())
LOG.debug("redeemAuthCode() {}", this);
if (authCode != null)
{
try
{
response = claimAuthCode(configuration);
if (LOG.isDebugEnabled())
LOG.debug("response: {}", response);
String idToken = (String)response.get("id_token");
if (idToken == null)
throw new AuthenticationException("no id_token");
String accessToken = (String)response.get("access_token");
if (accessToken == null)
throw new AuthenticationException("no access_token");
String tokenType = (String)response.get("token_type");
if (!"Bearer".equalsIgnoreCase(tokenType))
throw new AuthenticationException("invalid token_type");
claims = JwtDecoder.decode(idToken);
if (LOG.isDebugEnabled())
LOG.debug("claims {}", claims);
}
finally
{
// reset authCode as it can only be used once
authCode = null;
}
}
if (!verified)
{
validateClaims(configuration);
verified = true;
}
}
private void validateClaims(OpenIdConfiguration configuration) throws Exception
{
// Issuer Identifier for the OpenID Provider MUST exactly match the value of the iss (issuer) Claim.
if (!configuration.getIssuer().equals(claims.get("iss")))
throw new AuthenticationException("Issuer Identifier MUST exactly match the iss Claim");
// The aud (audience) Claim MUST contain the client_id value.
validateAudience(configuration);
// If an azp (authorized party) Claim is present, verify that its client_id is the Claim Value.
Object azp = claims.get("azp");
if (azp != null && !configuration.getClientId().equals(azp))
throw new AuthenticationException("Authorized party claim value should be the client_id");
// Check that the ID token has not expired by checking the exp claim.
long expiry = (Long)claims.get("exp");
long currentTimeSeconds = (long)(System.currentTimeMillis() / 1000F);
if (currentTimeSeconds > expiry)
throw new AuthenticationException("ID Token has expired");
}
private void validateAudience(OpenIdConfiguration configuration) throws AuthenticationException
{
Object aud = claims.get("aud");
String clientId = configuration.getClientId();
boolean isString = aud instanceof String;
boolean isList = aud instanceof Object[];
boolean isValidType = isString || isList;
if (isString && !clientId.equals(aud))
throw new AuthenticationException("Audience Claim MUST contain the client_id value");
else if (isList)
{
List<Object> list = Arrays.asList((Object[])aud);
if (!list.contains(clientId))
throw new AuthenticationException("Audience Claim MUST contain the client_id value");
if (list.size() > 1 && claims.get("azp") == null)
throw new AuthenticationException("A multi-audience ID token needs to contain an azp claim");
}
else if (!isValidType)
throw new AuthenticationException("Audience claim was not valid");
}
@SuppressWarnings("unchecked")
private Map<String, Object> claimAuthCode(OpenIdConfiguration configuration) throws Exception
{
Fields fields = new Fields();
fields.add("code", authCode);
fields.add("client_id", configuration.getClientId());
fields.add("client_secret", configuration.getClientSecret());
fields.add("redirect_uri", redirectUri);
fields.add("grant_type", "authorization_code");
FormRequestContent formContent = new FormRequestContent(fields);
Request request = configuration.getHttpClient().POST(configuration.getTokenEndpoint())
.body(formContent)
.timeout(10, TimeUnit.SECONDS);
ContentResponse response = request.send();
String responseBody = response.getContentAsString();
if (LOG.isDebugEnabled())
LOG.debug("Authentication response: {}", responseBody);
Object parsedResponse = new JSON().fromJSON(responseBody);
if (!(parsedResponse instanceof Map))
throw new AuthenticationException("Malformed response from OpenID Provider");
return (Map<String, Object>)parsedResponse;
}
public static class AuthenticationException extends Exception
{
public AuthenticationException(String message)
{
super(message);
}
}
}