|
1 | 1 | /*
|
2 |
| - * Copyright 2002-2017 the original author or authors. |
| 2 | + * Copyright 2002-2019 the original author or authors. |
3 | 3 | *
|
4 | 4 | * Licensed under the Apache License, Version 2.0 (the "License");
|
5 | 5 | * you may not use this file except in compliance with the License.
|
|
15 | 15 | */
|
16 | 16 | package org.springframework.security.oauth2.jwt;
|
17 | 17 |
|
18 |
| -import java.net.URL; |
19 | 18 | import java.time.Instant;
|
20 | 19 | import java.util.Collection;
|
21 | 20 | import java.util.Collections;
|
22 |
| -import java.util.HashMap; |
23 | 21 | import java.util.LinkedHashMap;
|
24 | 22 | import java.util.Map;
|
25 |
| -import java.util.stream.Collectors; |
26 |
| -import java.util.stream.Stream; |
| 23 | +import java.util.function.Consumer; |
27 | 24 |
|
28 |
| -import org.springframework.security.core.SpringSecurityCoreVersion; |
29 | 25 | import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
30 | 26 | import org.springframework.util.Assert;
|
31 | 27 |
|
| 28 | +import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD; |
| 29 | +import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP; |
| 30 | +import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT; |
| 31 | +import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS; |
| 32 | +import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI; |
| 33 | +import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF; |
| 34 | +import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB; |
| 35 | + |
32 | 36 | /**
|
33 | 37 | * An implementation of an {@link AbstractOAuth2Token} representing a JSON Web Token (JWT).
|
34 | 38 | *
|
|
47 | 51 | * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516">JSON Web Encryption (JWE)</a>
|
48 | 52 | */
|
49 | 53 | public class Jwt extends AbstractOAuth2Token implements JwtClaimAccessor {
|
50 |
| - private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; |
51 |
| - |
52 | 54 | private final Map<String, Object> headers;
|
53 | 55 | private final Map<String, Object> claims;
|
54 | 56 |
|
@@ -88,139 +90,181 @@ public Map<String, Object> getHeaders() {
|
88 | 90 | public Map<String, Object> getClaims() {
|
89 | 91 | return this.claims;
|
90 | 92 | }
|
91 |
| - |
92 |
| - public static Builder<?> builder() { |
93 |
| - return new Builder<>(); |
| 93 | + |
| 94 | + /** |
| 95 | + * Return a {@link Jwt.Builder} |
| 96 | + * |
| 97 | + * @return A {@link Jwt.Builder} |
| 98 | + */ |
| 99 | + public static Builder withTokenValue(String tokenValue) { |
| 100 | + return new Builder(tokenValue); |
94 | 101 | }
|
95 |
| - |
| 102 | + |
96 | 103 | /**
|
97 | 104 | * Helps configure a {@link Jwt}
|
98 | 105 | *
|
99 | 106 | * @author Jérôme Wacongne <ch4mp@c4-soft.com>
|
| 107 | + * @author Josh Cummings |
| 108 | + * @since 5.2 |
100 | 109 | */
|
101 |
| - public static class Builder<T extends Builder<T>> { |
102 |
| - protected String tokenValue; |
103 |
| - protected final Map<String, Object> claims = new HashMap<>(); |
104 |
| - protected final Map<String, Object> headers = new HashMap<>(); |
105 |
| - |
106 |
| - protected Builder() { |
107 |
| - } |
| 110 | + public final static class Builder { |
| 111 | + private String tokenValue; |
| 112 | + private final Map<String, Object> claims = new LinkedHashMap<>(); |
| 113 | + private final Map<String, Object> headers = new LinkedHashMap<>(); |
108 | 114 |
|
109 |
| - public T tokenValue(String tokenValue) { |
| 115 | + private Builder(String tokenValue) { |
110 | 116 | this.tokenValue = tokenValue;
|
111 |
| - return downcast(); |
112 | 117 | }
|
113 | 118 |
|
114 |
| - public T claim(String name, Object value) { |
115 |
| - this.claims.put(name, value); |
116 |
| - return downcast(); |
| 119 | + /** |
| 120 | + * Use this token value in the resulting {@link Jwt} |
| 121 | + * |
| 122 | + * @param tokenValue The token value to use |
| 123 | + * @return the {@link Builder} for further configurations |
| 124 | + */ |
| 125 | + public Builder tokenValue(String tokenValue) { |
| 126 | + this.tokenValue = tokenValue; |
| 127 | + return this; |
117 | 128 | }
|
118 | 129 |
|
119 |
| - public T clearClaims(Map<String, Object> claims) { |
120 |
| - this.claims.clear(); |
121 |
| - return downcast(); |
| 130 | + /** |
| 131 | + * Use this claim in the resulting {@link Jwt} |
| 132 | + * |
| 133 | + * @param name The claim name |
| 134 | + * @param value The claim value |
| 135 | + * @return the {@link Builder} for further configurations |
| 136 | + */ |
| 137 | + public Builder claim(String name, Object value) { |
| 138 | + this.claims.put(name, value); |
| 139 | + return this; |
122 | 140 | }
|
123 | 141 |
|
124 | 142 | /**
|
125 |
| - * Adds to existing claims (does not replace existing ones) |
126 |
| - * @param claims claims to add |
127 |
| - * @return this builder to further configure |
| 143 | + * Provides access to every {@link #claim(String, Object)} |
| 144 | + * declared so far with the possibility to add, replace, or remove. |
| 145 | + * @param claimsConsumer the consumer |
| 146 | + * @return the {@link Builder} for further configurations |
128 | 147 | */
|
129 |
| - public T claims(Map<String, Object> claims) { |
130 |
| - this.claims.putAll(claims); |
131 |
| - return downcast(); |
| 148 | + public Builder claims(Consumer<Map<String, Object>> claimsConsumer) { |
| 149 | + claimsConsumer.accept(this.claims); |
| 150 | + return this; |
132 | 151 | }
|
133 | 152 |
|
134 |
| - public T header(String name, Object value) { |
| 153 | + /** |
| 154 | + * Use this header in the resulting {@link Jwt} |
| 155 | + * |
| 156 | + * @param name The header name |
| 157 | + * @param value The header value |
| 158 | + * @return the {@link Builder} for further configurations |
| 159 | + */ |
| 160 | + public Builder header(String name, Object value) { |
135 | 161 | this.headers.put(name, value);
|
136 |
| - return downcast(); |
137 |
| - } |
138 |
| - |
139 |
| - public T clearHeaders(Map<String, Object> headers) { |
140 |
| - this.headers.clear(); |
141 |
| - return downcast(); |
| 162 | + return this; |
142 | 163 | }
|
143 | 164 |
|
144 | 165 | /**
|
145 |
| - * Adds to existing headers (does not replace existing ones) |
146 |
| - * @param headers headers to add |
147 |
| - * @return this builder to further configure |
| 166 | + * Provides access to every {@link #header(String, Object)} |
| 167 | + * declared so far with the possibility to add, replace, or remove. |
| 168 | + * @param headersConsumer the consumer |
| 169 | + * @return the {@link Builder} for further configurations |
148 | 170 | */
|
149 |
| - public T headers(Map<String, Object> headers) { |
150 |
| - headers.entrySet().stream().forEach(e -> this.header(e.getKey(), e.getValue())); |
151 |
| - return downcast(); |
152 |
| - } |
153 |
| - |
154 |
| - public Jwt build() { |
155 |
| - final JwtClaimSet claimSet = new JwtClaimSet(claims); |
156 |
| - return new Jwt( |
157 |
| - this.tokenValue, |
158 |
| - claimSet.getClaimAsInstant(JwtClaimNames.IAT), |
159 |
| - claimSet.getClaimAsInstant(JwtClaimNames.EXP), |
160 |
| - this.headers, |
161 |
| - claimSet); |
162 |
| - } |
163 |
| - |
164 |
| - public T audience(Stream<String> audience) { |
165 |
| - this.claim(JwtClaimNames.AUD, audience.collect(Collectors.toList())); |
166 |
| - return downcast(); |
167 |
| - } |
168 |
| - |
169 |
| - public T audience(Collection<String> audience) { |
170 |
| - return audience(audience.stream()); |
| 171 | + public Builder headers(Consumer<Map<String, Object>> headersConsumer) { |
| 172 | + headersConsumer.accept(this.headers); |
| 173 | + return this; |
171 | 174 | }
|
172 | 175 |
|
173 |
| - public T audience(String... audience) { |
174 |
| - return audience(Stream.of(audience)); |
| 176 | + /** |
| 177 | + * Use this audience in the resulting {@link Jwt} |
| 178 | + * |
| 179 | + * @param audience The audience(s) to use |
| 180 | + * @return the {@link Builder} for further configurations |
| 181 | + */ |
| 182 | + public Builder audience(Collection<String> audience) { |
| 183 | + return claim(AUD, audience); |
175 | 184 | }
|
176 | 185 |
|
177 |
| - public T expiresAt(Instant expiresAt) { |
178 |
| - this.claim(JwtClaimNames.EXP, expiresAt.getEpochSecond()); |
179 |
| - return downcast(); |
| 186 | + /** |
| 187 | + * Use this expiration in the resulting {@link Jwt} |
| 188 | + * |
| 189 | + * @param expiresAt The expiration to use |
| 190 | + * @return the {@link Builder} for further configurations |
| 191 | + */ |
| 192 | + public Builder expiresAt(Instant expiresAt) { |
| 193 | + this.claim(EXP, expiresAt); |
| 194 | + return this; |
180 | 195 | }
|
181 | 196 |
|
182 |
| - public T jti(String jti) { |
183 |
| - this.claim(JwtClaimNames.JTI, jti); |
184 |
| - return downcast(); |
| 197 | + /** |
| 198 | + * Use this identifier in the resulting {@link Jwt} |
| 199 | + * |
| 200 | + * @param jti The identifier to use |
| 201 | + * @return the {@link Builder} for further configurations |
| 202 | + */ |
| 203 | + public Builder jti(String jti) { |
| 204 | + this.claim(JTI, jti); |
| 205 | + return this; |
185 | 206 | }
|
186 | 207 |
|
187 |
| - public T issuedAt(Instant issuedAt) { |
188 |
| - this.claim(JwtClaimNames.IAT, issuedAt.getEpochSecond()); |
189 |
| - return downcast(); |
| 208 | + /** |
| 209 | + * Use this issued-at timestamp in the resulting {@link Jwt} |
| 210 | + * |
| 211 | + * @param issuedAt The issued-at timestamp to use |
| 212 | + * @return the {@link Builder} for further configurations |
| 213 | + */ |
| 214 | + public Builder issuedAt(Instant issuedAt) { |
| 215 | + this.claim(IAT, issuedAt); |
| 216 | + return this; |
190 | 217 | }
|
191 | 218 |
|
192 |
| - public T issuer(URL issuer) { |
193 |
| - this.claim(JwtClaimNames.ISS, issuer.toExternalForm()); |
194 |
| - return downcast(); |
| 219 | + /** |
| 220 | + * Use this issuer in the resulting {@link Jwt} |
| 221 | + * |
| 222 | + * @param issuer The issuer to use |
| 223 | + * @return the {@link Builder} for further configurations |
| 224 | + */ |
| 225 | + public Builder issuer(String issuer) { |
| 226 | + this.claim(ISS, issuer); |
| 227 | + return this; |
195 | 228 | }
|
196 | 229 |
|
197 |
| - public T notBefore(Instant notBefore) { |
198 |
| - this.claim(JwtClaimNames.NBF, notBefore.getEpochSecond()); |
199 |
| - return downcast(); |
| 230 | + /** |
| 231 | + * Use this not-before timestamp in the resulting {@link Jwt} |
| 232 | + * |
| 233 | + * @param notBefore The not-before timestamp to use |
| 234 | + * @return the {@link Builder} for further configurations |
| 235 | + */ |
| 236 | + public Builder notBefore(Instant notBefore) { |
| 237 | + this.claim(NBF, notBefore.getEpochSecond()); |
| 238 | + return this; |
200 | 239 | }
|
201 | 240 |
|
202 |
| - public T subject(String subject) { |
203 |
| - this.claim(JwtClaimNames.SUB, subject); |
204 |
| - return downcast(); |
205 |
| - } |
206 |
| - |
207 |
| - @SuppressWarnings("unchecked") |
208 |
| - protected T downcast() { |
209 |
| - return (T) this; |
| 241 | + /** |
| 242 | + * Use this subject in the resulting {@link Jwt} |
| 243 | + * |
| 244 | + * @param subject The subject to use |
| 245 | + * @return the {@link Builder} for further configurations |
| 246 | + */ |
| 247 | + public Builder subject(String subject) { |
| 248 | + this.claim(SUB, subject); |
| 249 | + return this; |
210 | 250 | }
|
211 |
| - } |
212 | 251 |
|
213 |
| - private static final class JwtClaimSet extends HashMap<String, Object> implements JwtClaimAccessor { |
214 |
| - private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; |
215 |
| - |
216 |
| - public JwtClaimSet(Map<String, Object> claims) { |
217 |
| - super(claims); |
| 252 | + /** |
| 253 | + * Build the {@link Jwt} |
| 254 | + * |
| 255 | + * @return The constructed {@link Jwt} |
| 256 | + */ |
| 257 | + public Jwt build() { |
| 258 | + Instant iat = toInstant(this.claims.get(IAT)); |
| 259 | + Instant exp = toInstant(this.claims.get(EXP)); |
| 260 | + return new Jwt(this.tokenValue, iat, exp, this.headers, this.claims); |
218 | 261 | }
|
219 | 262 |
|
220 |
| - @Override |
221 |
| - public Map<String, Object> getClaims() { |
222 |
| - return this; |
| 263 | + private Instant toInstant(Object timestamp) { |
| 264 | + if (timestamp != null) { |
| 265 | + Assert.isInstanceOf(Instant.class, timestamp, "timestamps must be of type Instant"); |
| 266 | + } |
| 267 | + return (Instant) timestamp; |
223 | 268 | }
|
224 |
| - |
225 | 269 | }
|
226 | 270 | }
|
0 commit comments