Skip to content

Commit 4aab314

Browse files
dkiroagologan
authored andcommitted
Add support for reading custom claims and make IdToken public (openid#759)
1 parent 5692d83 commit 4aab314

File tree

4 files changed

+224
-12
lines changed

4 files changed

+224
-12
lines changed

library/java/net/openid/appauth/AuthState.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,27 @@ public String getIdToken() {
270270
return null;
271271
}
272272

273+
/**
274+
* The current parsed ID token, if available.
275+
*/
276+
@Nullable
277+
public IdToken getParsedIdToken() {
278+
String stringToken = getIdToken();
279+
IdToken token;
280+
281+
if (stringToken != null) {
282+
try {
283+
token = IdToken.from(stringToken);
284+
} catch (JSONException | IdToken.IdTokenException ex) {
285+
token = null;
286+
}
287+
} else {
288+
token = null;
289+
}
290+
291+
return token;
292+
}
293+
273294
/**
274295
* The current client secret, if available.
275296
*/

library/java/net/openid/appauth/IdToken.java

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
package net.openid.appauth;
1616

17+
import static net.openid.appauth.AdditionalParamsProcessor.builtInParams;
18+
1719
import android.net.Uri;
1820
import android.text.TextUtils;
1921
import android.util.Base64;
@@ -26,7 +28,10 @@
2628
import org.json.JSONObject;
2729

2830
import java.util.ArrayList;
31+
import java.util.Collections;
2932
import java.util.List;
33+
import java.util.Map;
34+
import java.util.Set;
3035

3136
/**
3237
* An OpenID Connect ID Token. Contains claims about the authentication of an End-User by an
@@ -38,7 +43,7 @@
3843
* @see "OpenID Connect Core ID Token Validation, Section 3.1.3.7
3944
* <http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation>"
4045
*/
41-
class IdToken {
46+
public class IdToken {
4247

4348
private static final String KEY_ISSUER = "iss";
4449
private static final String KEY_SUBJECT = "sub";
@@ -50,41 +55,107 @@ class IdToken {
5055
private static final Long MILLIS_PER_SECOND = 1000L;
5156
private static final Long TEN_MINUTES_IN_SECONDS = 600L;
5257

58+
private static final Set<String> BUILT_IN_CLAIMS = builtInParams(
59+
KEY_ISSUER,
60+
KEY_SUBJECT,
61+
KEY_AUDIENCE,
62+
KEY_EXPIRATION,
63+
KEY_ISSUED_AT,
64+
KEY_NONCE,
65+
KEY_AUTHORIZED_PARTY);
66+
67+
/**
68+
* Issuer Identifier for the Issuer of the response.
69+
*/
70+
@NonNull
5371
public final String issuer;
72+
73+
/**
74+
* Subject Identifier. A locally unique and never reassigned identifier within the Issuer
75+
* for the End-User.
76+
*/
77+
@NonNull
5478
public final String subject;
79+
80+
/**
81+
* Audience(s) that this ID Token is intended for.
82+
*/
83+
@NonNull
5584
public final List<String> audience;
85+
86+
/**
87+
* Expiration time on or after which the ID Token MUST NOT be accepted for processing.
88+
*/
89+
@NonNull
5690
public final Long expiration;
91+
92+
/**
93+
* Time at which the JWT was issued.
94+
*/
95+
@NonNull
5796
public final Long issuedAt;
97+
98+
/**
99+
* String value used to associate a Client session with an ID Token,
100+
* and to mitigate replay attacks.
101+
*/
102+
@Nullable
58103
public final String nonce;
104+
105+
/**
106+
* Authorized party - the party to which the ID Token was issued.
107+
* If present, it MUST contain the OAuth 2.0 Client ID of this party.
108+
*/
109+
@Nullable
59110
public final String authorizedParty;
60111

112+
/**
113+
* Additional claims present in this ID Token.
114+
*/
115+
@NonNull
116+
public final Map<String, Object> additionalClaims;
117+
61118
@VisibleForTesting
62119
IdToken(@NonNull String issuer,
63120
@NonNull String subject,
64121
@NonNull List<String> audience,
65122
@NonNull Long expiration,
66123
@NonNull Long issuedAt) {
67-
this(issuer, subject, audience, expiration, issuedAt, null, null);
124+
this(issuer, subject, audience, expiration, issuedAt, null, null, Collections.emptyMap());
68125
}
69126

127+
@VisibleForTesting
70128
IdToken(@NonNull String issuer,
71129
@NonNull String subject,
72130
@NonNull List<String> audience,
73131
@NonNull Long expiration,
74132
@NonNull Long issuedAt,
75133
@Nullable String nonce,
76134
@Nullable String authorizedParty) {
135+
this(issuer, subject, audience, expiration, issuedAt,
136+
nonce, authorizedParty, Collections.emptyMap());
137+
}
138+
139+
IdToken(@NonNull String issuer,
140+
@NonNull String subject,
141+
@NonNull List<String> audience,
142+
@NonNull Long expiration,
143+
@NonNull Long issuedAt,
144+
@Nullable String nonce,
145+
@Nullable String authorizedParty,
146+
@NonNull Map<String, Object> additionalClaims) {
77147
this.issuer = issuer;
78148
this.subject = subject;
79149
this.audience = audience;
80150
this.expiration = expiration;
81151
this.issuedAt = issuedAt;
82152
this.nonce = nonce;
83153
this.authorizedParty = authorizedParty;
154+
this.additionalClaims = additionalClaims;
84155
}
85156

86157
private static JSONObject parseJwtSection(String section) throws JSONException {
87-
byte[] decodedSection = Base64.decode(section,Base64.URL_SAFE);
158+
byte[] decodedSection = Base64.decode(section, Base64.URL_SAFE);
88159
String jsonString = new String(decodedSection);
89160
return new JSONObject(jsonString);
90161
}
@@ -100,19 +171,24 @@ static IdToken from(String token) throws JSONException, IdTokenException {
100171
parseJwtSection(sections[0]);
101172
JSONObject claims = parseJwtSection(sections[1]);
102173

103-
String issuer = JsonUtil.getString(claims, KEY_ISSUER);
104-
String subject = JsonUtil.getString(claims, KEY_SUBJECT);
174+
final String issuer = JsonUtil.getString(claims, KEY_ISSUER);
175+
final String subject = JsonUtil.getString(claims, KEY_SUBJECT);
105176
List<String> audience;
106177
try {
107178
audience = JsonUtil.getStringList(claims, KEY_AUDIENCE);
108179
} catch (JSONException jsonEx) {
109180
audience = new ArrayList<>();
110181
audience.add(JsonUtil.getString(claims, KEY_AUDIENCE));
111182
}
112-
Long expiration = claims.getLong(KEY_EXPIRATION);
113-
Long issuedAt = claims.getLong(KEY_ISSUED_AT);
114-
String nonce = JsonUtil.getStringIfDefined(claims, KEY_NONCE);
115-
String authorizedParty = JsonUtil.getStringIfDefined(claims, KEY_AUTHORIZED_PARTY);
183+
final Long expiration = claims.getLong(KEY_EXPIRATION);
184+
final Long issuedAt = claims.getLong(KEY_ISSUED_AT);
185+
final String nonce = JsonUtil.getStringIfDefined(claims, KEY_NONCE);
186+
final String authorizedParty = JsonUtil.getStringIfDefined(claims, KEY_AUTHORIZED_PARTY);
187+
188+
for (String key: BUILT_IN_CLAIMS) {
189+
claims.remove(key);
190+
}
191+
Map<String, Object> additionalClaims = JsonUtil.toMap(claims);
116192

117193
return new IdToken(
118194
issuer,
@@ -121,7 +197,8 @@ static IdToken from(String token) throws JSONException, IdTokenException {
121197
expiration,
122198
issuedAt,
123199
nonce,
124-
authorizedParty
200+
authorizedParty,
201+
additionalClaims
125202
);
126203
}
127204

library/java/net/openid/appauth/JsonUtil.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@
1919
import android.net.Uri;
2020
import androidx.annotation.NonNull;
2121
import androidx.annotation.Nullable;
22+
import androidx.annotation.VisibleForTesting;
2223

2324
import org.json.JSONArray;
2425
import org.json.JSONException;
2526
import org.json.JSONObject;
2627

2728
import java.util.ArrayList;
29+
import java.util.Collection;
30+
import java.util.HashMap;
2831
import java.util.Iterator;
2932
import java.util.LinkedHashMap;
3033
import java.util.List;
@@ -160,6 +163,34 @@ public static void putIfNotNull(
160163
}
161164
}
162165

166+
@VisibleForTesting
167+
static void putIfNotNull(
168+
@NonNull JSONObject json,
169+
@NonNull String field,
170+
@Nullable Object value) {
171+
checkNotNull(json, "json must not be null");
172+
checkNotNull(field, "field must not be null");
173+
if (value == null) {
174+
return;
175+
}
176+
try {
177+
if (value instanceof Collection) {
178+
json.put(field, new JSONArray((Collection) value));
179+
} else if (value instanceof Map) {
180+
Map<String, Object> map = (Map<String, Object>)value;
181+
JSONObject valueObj = new JSONObject();
182+
for (String key : map.keySet()) {
183+
JsonUtil.putIfNotNull(valueObj, key, map.get(key));
184+
}
185+
json.put(field, valueObj);
186+
} else {
187+
json.put(field, value);
188+
}
189+
} catch (JSONException ex) {
190+
throw new IllegalStateException("JSONException thrown in violation of contract", ex);
191+
}
192+
}
193+
163194
@NonNull
164195
public static String getString(
165196
@NonNull JSONObject json,
@@ -336,6 +367,42 @@ public static List<String> toStringList(@Nullable JSONArray jsonArray)
336367
return arrayList;
337368
}
338369

370+
@NonNull
371+
public static Map<String, Object> toMap(@NonNull JSONObject json) throws JSONException {
372+
checkNotNull(json, "json must not be null");
373+
374+
Map<String, Object> map = new HashMap<>();
375+
Iterator<String> keys = json.keys();
376+
while (keys.hasNext()) {
377+
String key = keys.next();
378+
Object value = json.get(key);
379+
if (value instanceof JSONArray) {
380+
value = toList((JSONArray) value);
381+
} else if (value instanceof JSONObject) {
382+
value = toMap((JSONObject) value);
383+
}
384+
map.put(key, value);
385+
}
386+
return map;
387+
}
388+
389+
@NonNull
390+
public static List<Object> toList(@NonNull JSONArray jsonArray) throws JSONException {
391+
checkNotNull(jsonArray, "jsonArray must not be null");
392+
393+
List<Object> list = new ArrayList<>();
394+
for (int i = 0; i < jsonArray.length(); i++) {
395+
Object value = jsonArray.get(i);
396+
if (value instanceof JSONArray) {
397+
value = toList((JSONArray) value);
398+
} else if (value instanceof JSONObject) {
399+
value = toMap((JSONObject) value);
400+
}
401+
list.add(value);
402+
}
403+
return list;
404+
}
405+
339406
@NonNull
340407
public static List<Uri> toUriList(@Nullable JSONArray jsonArray)
341408
throws JSONException {

0 commit comments

Comments
 (0)