|
@@ -0,0 +1,349 @@
|
|
|
+/*
|
|
|
+ * Copyright 2020-2021 the original author or authors.
|
|
|
+ *
|
|
|
+ * Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
+ * you may not use this file except in compliance with the License.
|
|
|
+ * You may obtain a copy of the License at
|
|
|
+ *
|
|
|
+ * https://www.apache.org/licenses/LICENSE-2.0
|
|
|
+ *
|
|
|
+ * Unless required by applicable law or agreed to in writing, software
|
|
|
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
+ * See the License for the specific language governing permissions and
|
|
|
+ * limitations under the License.
|
|
|
+ */
|
|
|
+package org.springframework.security.oauth2.core.oidc;
|
|
|
+
|
|
|
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
|
|
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
|
|
+import org.springframework.security.oauth2.core.Version;
|
|
|
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
|
|
+import org.springframework.util.Assert;
|
|
|
+
|
|
|
+import java.io.Serializable;
|
|
|
+import java.net.URI;
|
|
|
+import java.net.URL;
|
|
|
+import java.time.Instant;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.LinkedList;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.function.Consumer;
|
|
|
+
|
|
|
+/**
|
|
|
+ * A representation of an OpenID Client Registration Request and Response,
|
|
|
+ * which contains a set of claims defined by the
|
|
|
+ * OpenID Connect Registration 1.0 specification.
|
|
|
+ *
|
|
|
+ * @author Ovidiu Popa
|
|
|
+ * @since 0.1.1
|
|
|
+ * @see OidcClientMetadataClaimAccessor
|
|
|
+ * @see <a href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration">3.1. Client Registration Request</a>
|
|
|
+ */
|
|
|
+public final class OidcClientRegistration implements OidcClientMetadataClaimAccessor, Serializable {
|
|
|
+ private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
|
|
+ private final Map<String, Object> claims;
|
|
|
+
|
|
|
+ private OidcClientRegistration(Map<String, Object> claims) {
|
|
|
+ this.claims = Collections.unmodifiableMap(claims);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns the OpenID Client Registration metadata.
|
|
|
+ *
|
|
|
+ * @return a {@code Map} of the metadata values
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> getClaims() {
|
|
|
+ return this.claims;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Constructs a new {@link OidcClientRegistration.Builder} with empty claims.
|
|
|
+ *
|
|
|
+ * @return the {@link OidcClientRegistration.Builder}
|
|
|
+ */
|
|
|
+ public static Builder builder() {
|
|
|
+ return new Builder();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Constructs a new {@link Builder} with the provided claims.
|
|
|
+ *
|
|
|
+ * @param claims the claims to initialize the builder
|
|
|
+ */
|
|
|
+ public static Builder withClaims(Map<String, Object> claims) {
|
|
|
+ Assert.notEmpty(claims, "claims cannot be empty");
|
|
|
+ return new Builder()
|
|
|
+ .claims(c -> c.putAll(claims));
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class Builder {
|
|
|
+
|
|
|
+ private final Map<String, Object> claims = new LinkedHashMap<>();
|
|
|
+
|
|
|
+ private Builder() {
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add this Redirect URI to the collection of {@code redirect_uris} in the resulting
|
|
|
+ * {@link OidcClientRegistration}, REQUIRED.
|
|
|
+ *
|
|
|
+ * @param redirectUri the OAuth 2.0 {@code redirect_uri} value that client supports
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder redirectUri(String redirectUri) {
|
|
|
+ addClaimToClaimList(OidcClientMetadataClaimNames.REDIRECT_URIS, redirectUri);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A {@code Consumer} of the Redirect URI(s) allowing the ability to add, replace, or remove.
|
|
|
+ *
|
|
|
+ * @param redirectUriConsumer a {@code Consumer} of the Redirect URI(s)
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder redirectUris(Consumer<List<String>> redirectUriConsumer) {
|
|
|
+ acceptClaimValues(OidcClientMetadataClaimNames.REDIRECT_URIS, redirectUriConsumer);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add this Response Type to the collection of {@code response_types} in the resulting
|
|
|
+ * {@link OidcClientRegistration}, OPTIONAL.
|
|
|
+ *
|
|
|
+ * @param responseType the OAuth 2.0 {@code response_type} value that client supports
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder responseType(String responseType) {
|
|
|
+ addClaimToClaimList(OidcClientMetadataClaimNames.RESPONSE_TYPES, responseType);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add {@code Consumer} of {@code response_types} allowing the ability to add, replace, or remove
|
|
|
+ * {@link OidcClientRegistration}, OPTIONAL.
|
|
|
+ *
|
|
|
+ * @param responseType the OAuth 2.0 {@code response_type} value that client supports
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder responseTypes(Consumer<List<String>> responseType) {
|
|
|
+ acceptClaimValues(OidcClientMetadataClaimNames.RESPONSE_TYPES, responseType);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets {@code client_name} claim in the resulting
|
|
|
+ * {@link OidcClientRegistration}, OPTIONAL.
|
|
|
+ *
|
|
|
+ * @param clientName the OAuth 2.0 {@code client_name} of the registered client
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder clientName(String clientName) {
|
|
|
+ return claim(OidcClientMetadataClaimNames.CLIENT_NAME, clientName);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets {@code client_id} claim in the resulting
|
|
|
+ * {@link OidcClientRegistration}.
|
|
|
+ *
|
|
|
+ * @param clientId the OAuth 2.0 {@code client_id} of the registered client
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder clientId(String clientId) {
|
|
|
+ return claim(OidcClientMetadataClaimNames.CLIENT_ID, clientId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets {@code client_id_issued_at} claim in the resulting
|
|
|
+ * {@link OidcClientRegistration}.
|
|
|
+ *
|
|
|
+ * @param clientIssuedAt the timestamp {@code client_id_issued_at} when the client was issued
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder clientIdIssuedAt(Instant clientIssuedAt) {
|
|
|
+ return claim(OidcClientMetadataClaimNames.CLIENT_ID_ISSUED_AT, clientIssuedAt);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets {@code client_secret} claim in the resulting
|
|
|
+ * {@link OidcClientRegistration}.
|
|
|
+ *
|
|
|
+ * @param clientSecret the {@code client_secret} of the registered client
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder clientSecret(String clientSecret) {
|
|
|
+ return claim(OidcClientMetadataClaimNames.CLIENT_SECRET, clientSecret);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets {@code client_secret_expires_at} claim in the resulting
|
|
|
+ * {@link OidcClientRegistration}.
|
|
|
+ *
|
|
|
+ * @param clientSecretExpiresAt the timestamp {@code client_secret_expires_at} when the client_secret expires
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder clientSecretExpiresAt(Instant clientSecretExpiresAt) {
|
|
|
+ return claim(OidcClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT, clientSecretExpiresAt);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add this Grant Type to the collection of {@code grant_types_supported} in the resulting
|
|
|
+ * {@link OidcClientRegistration}, OPTIONAL.
|
|
|
+ *
|
|
|
+ * @param grantType the OAuth 2.0 {@code grant_type} value that client supports
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder grantType(String grantType) {
|
|
|
+ addClaimToClaimList(OidcClientMetadataClaimNames.GRANT_TYPES, grantType);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A {@code Consumer} of the Grant Type(s) allowing the ability to add, replace, or remove.
|
|
|
+ *
|
|
|
+ * @param grantTypesConsumer a {@code Consumer} of the Grant Type(s)
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder grantTypes(Consumer<List<String>> grantTypesConsumer) {
|
|
|
+ acceptClaimValues(OidcClientMetadataClaimNames.GRANT_TYPES, grantTypesConsumer);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add this Scope to the collection of {@code scopes_supported} in the resulting
|
|
|
+ * {@link OidcClientRegistration}, RECOMMENDED.
|
|
|
+ *
|
|
|
+ * @param scope the OAuth 2.0 {@code scope} value that client supports
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder scope(String scope) {
|
|
|
+ claim(OidcClientMetadataClaimNames.SCOPE, scope);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add {@code Consumer} of {@code scopes} allowing the ability to add, replace, or remove
|
|
|
+ * {@link OidcClientRegistration}, RECOMMENDED.
|
|
|
+ *
|
|
|
+ * @param scopesConsumer the OAuth 2.0 {@code scope} value that client supports
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder scopes(Consumer<List<String>> scopesConsumer) {
|
|
|
+ acceptClaimValues(OidcClientMetadataClaimNames.SCOPE, scopesConsumer);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add this Token endpoint authentication method to the collection of {@code token_endpoint_auth_method} in the resulting
|
|
|
+ * {@link OidcClientRegistration}, OPTIONAL.
|
|
|
+ *
|
|
|
+ * @param tokenEndpointAuthenticationMethod the OAuth 2.0 {@code token_endpoint_auth_method} value that client supports
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder tokenEndpointAuthenticationMethod(String tokenEndpointAuthenticationMethod) {
|
|
|
+ claim(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, tokenEndpointAuthenticationMethod);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add this claim in the resulting {@link OidcClientRegistration}.
|
|
|
+ *
|
|
|
+ * @param name the claim name
|
|
|
+ * @param value the claim value
|
|
|
+ * @return the {@link Builder} for further configuration
|
|
|
+ */
|
|
|
+ public Builder claim(String name, Object value) {
|
|
|
+ Assert.hasText(name, "name cannot be empty");
|
|
|
+ Assert.notNull(value, "value cannot be null");
|
|
|
+ this.claims.put(name, value);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Provides access to every {@link #claim(String, Object)} declared so far with
|
|
|
+ * the possibility to add, replace, or remove.
|
|
|
+ *
|
|
|
+ * @param claimsConsumer a {@code Consumer} of the claims
|
|
|
+ * @return the {@link Builder} for further configurations
|
|
|
+ */
|
|
|
+ public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
|
|
|
+ claimsConsumer.accept(this.claims);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public OidcClientRegistration build() {
|
|
|
+ this.claims.computeIfAbsent(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD,
|
|
|
+ k -> ClientAuthenticationMethod.BASIC.getValue());
|
|
|
+ // If omitted, the default is that the Client will use only the authorization_code Grant Type.
|
|
|
+ this.claims.computeIfAbsent(OidcClientMetadataClaimNames.GRANT_TYPES,
|
|
|
+ k -> Collections.singletonList(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
|
|
|
+ //If omitted, the default is that the Client will use only the code Response Type.
|
|
|
+ this.claims.computeIfAbsent(OidcClientMetadataClaimNames.RESPONSE_TYPES,
|
|
|
+ k -> Collections.singletonList(OAuth2AuthorizationResponseType.CODE.getValue()));
|
|
|
+ validateRedirectUris();
|
|
|
+ validateReponseTypesClaim();
|
|
|
+ validateGrantTypesClaim();
|
|
|
+ return new OidcClientRegistration(this.claims);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateRedirectUris() {
|
|
|
+ // redirect_uris is required
|
|
|
+ Assert.notNull(this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris cannot be null");
|
|
|
+ Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris must be of type list");
|
|
|
+ Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris must not be empty");
|
|
|
+ ((List<?>) this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS)).forEach(
|
|
|
+ url -> validateURL(url, "redirect_uri must be a valid URL")
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateGrantTypesClaim() {
|
|
|
+ Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES), "grant_types must be of type List");
|
|
|
+ List<?> grantTypes = (List<?>) this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES);
|
|
|
+ // If empty, the default is that the Client will use only the authorization_code Grant Type.
|
|
|
+ if (grantTypes.isEmpty()) {
|
|
|
+ this.claims.put(OidcClientMetadataClaimNames.GRANT_TYPES,
|
|
|
+ Collections.singletonList(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateReponseTypesClaim() {
|
|
|
+ Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.RESPONSE_TYPES), "response_types must be of type List");
|
|
|
+ List<?> responseTypes = (List<?>) this.claims.get(OidcClientMetadataClaimNames.RESPONSE_TYPES);
|
|
|
+ //If empty, the default is that the Client will use only the code Response Type.
|
|
|
+ if (responseTypes.isEmpty()) {
|
|
|
+ this.claims.put(OidcClientMetadataClaimNames.RESPONSE_TYPES, Collections.singletonList(OAuth2AuthorizationResponseType.CODE.getValue()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void validateURL(Object url, String errorMessage) {
|
|
|
+ if (URL.class.isAssignableFrom(url.getClass())) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ new URI(url.toString()).toURL();
|
|
|
+ } catch (Exception ex) {
|
|
|
+ throw new IllegalArgumentException(errorMessage, ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private void addClaimToClaimList(String name, String value) {
|
|
|
+ Assert.hasText(name, "name cannot be empty");
|
|
|
+ Assert.notNull(value, "value cannot be null");
|
|
|
+ this.claims.computeIfAbsent(name, k -> new LinkedList<String>());
|
|
|
+ ((List<String>) this.claims.get(name)).add(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private void acceptClaimValues(String name, Consumer<List<String>> valuesConsumer) {
|
|
|
+ Assert.hasText(name, "name cannot be empty");
|
|
|
+ Assert.notNull(valuesConsumer, "valuesConsumer cannot be null");
|
|
|
+ this.claims.computeIfAbsent(name, k -> new LinkedList<String>());
|
|
|
+ List<String> values = (List<String>) this.claims.get(name);
|
|
|
+ valuesConsumer.accept(values);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|