Enable null-safety in spring-security-oauth2-core

Closes gh-17820
This commit is contained in:
Joe Grandja 2026-01-29 09:36:33 -05:00 committed by Robert Winch
parent dfed528851
commit db5310bee8
No known key found for this signature in database
52 changed files with 735 additions and 391 deletions

View File

@ -1,5 +1,6 @@
plugins {
id 'compile-warnings-error'
id 'security-nullability'
}
apply plugin: 'io.spring.convention.spring-module'

View File

@ -19,7 +19,8 @@ package org.springframework.security.oauth2.core;
import java.io.Serializable;
import java.time.Instant;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
@ -37,9 +38,9 @@ public abstract class AbstractOAuth2Token implements OAuth2Token, Serializable {
private final String tokenValue;
private final Instant issuedAt;
private final @Nullable Instant issuedAt;
private final Instant expiresAt;
private final @Nullable Instant expiresAt;
/**
* Sub-class constructor.
@ -78,8 +79,7 @@ public abstract class AbstractOAuth2Token implements OAuth2Token, Serializable {
* Returns the time at which the token was issued.
* @return the time the token was issued or {@code null}
*/
@Nullable
public Instant getIssuedAt() {
public @Nullable Instant getIssuedAt() {
return this.issuedAt;
}
@ -87,8 +87,7 @@ public abstract class AbstractOAuth2Token implements OAuth2Token, Serializable {
* Returns the expiration time on or after which the token MUST NOT be accepted.
* @return the token expiration time or {@code null}
*/
@Nullable
public Instant getExpiresAt() {
public @Nullable Instant getExpiresAt() {
return this.expiresAt;
}

View File

@ -21,6 +21,8 @@ import java.time.Instant;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
import org.springframework.util.Assert;
@ -44,11 +46,11 @@ public interface ClaimAccessor {
* type {@code T}.
* @param claim the name of the claim
* @param <T> the type of the claim value
* @return the claim value
* @return the claim value, or {@code null} if the claim does not exist
* @since 5.2
*/
@SuppressWarnings("unchecked")
default <T> T getClaim(String claim) {
default <T> @Nullable T getClaim(String claim) {
return !hasClaim(claim) ? null : (T) getClaims().get(claim);
}
@ -71,7 +73,7 @@ public interface ClaimAccessor {
* @return the claim value or {@code null} if it does not exist or is equal to
* {@code null}
*/
default String getClaimAsString(String claim) {
default @Nullable String getClaimAsString(String claim) {
return !hasClaim(claim) ? null
: ClaimConversionService.getSharedInstance().convert(getClaims().get(claim), String.class);
}
@ -85,7 +87,8 @@ public interface ClaimAccessor {
* {@code Boolean}
* @throws NullPointerException if the claim value is {@code null}
*/
default Boolean getClaimAsBoolean(String claim) {
@SuppressWarnings("NullAway")
default @Nullable Boolean getClaimAsBoolean(String claim) {
if (!hasClaim(claim)) {
return null;
}
@ -100,8 +103,12 @@ public interface ClaimAccessor {
* Returns the claim value as an {@code Instant} or {@code null} if it does not exist.
* @param claim the name of the claim
* @return the claim value or {@code null} if it does not exist
* @throws IllegalArgumentException if the claim value cannot be converted to an
* {@code Instant}
* @throws NullPointerException if the claim value is {@code null}
*/
default Instant getClaimAsInstant(String claim) {
@SuppressWarnings("NullAway")
default @Nullable Instant getClaimAsInstant(String claim) {
if (!hasClaim(claim)) {
return null;
}
@ -116,8 +123,12 @@ public interface ClaimAccessor {
* Returns the claim value as an {@code URL} or {@code null} if it does not exist.
* @param claim the name of the claim
* @return the claim value or {@code null} if it does not exist
* @throws IllegalArgumentException if the claim value cannot be converted to a
* {@code URL}
* @throws NullPointerException if the claim value is {@code null}
*/
default URL getClaimAsURL(String claim) {
@SuppressWarnings("NullAway")
default @Nullable URL getClaimAsURL(String claim) {
if (!hasClaim(claim)) {
return null;
}
@ -137,8 +148,8 @@ public interface ClaimAccessor {
* {@code Map}
* @throws NullPointerException if the claim value is {@code null}
*/
@SuppressWarnings("unchecked")
default Map<String, Object> getClaimAsMap(String claim) {
@SuppressWarnings({ "unchecked", "NullAway" })
default @Nullable Map<String, Object> getClaimAsMap(String claim) {
if (!hasClaim(claim)) {
return null;
}
@ -162,8 +173,8 @@ public interface ClaimAccessor {
* {@code List}
* @throws NullPointerException if the claim value is {@code null}
*/
@SuppressWarnings("unchecked")
default List<String> getClaimAsStringList(String claim) {
@SuppressWarnings({ "unchecked", "NullAway" })
default @Nullable List<String> getClaimAsStringList(String claim) {
if (!hasClaim(claim)) {
return null;
}

View File

@ -18,7 +18,6 @@ package org.springframework.security.oauth2.core;
import java.io.Serializable;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
/**
@ -105,7 +104,6 @@ public final class ClientAuthenticationMethod implements Serializable {
* constant, if any
* @since 6.5
*/
@NonNull
public static ClientAuthenticationMethod valueOf(String method) {
for (ClientAuthenticationMethod m : methods()) {
if (m.getValue().equals(method)) {

View File

@ -22,6 +22,8 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.util.Assert;
@ -58,17 +60,21 @@ public final class DefaultOAuth2AuthenticatedPrincipal implements OAuth2Authenti
/**
* Constructs an {@code DefaultOAuth2AuthenticatedPrincipal} using the provided
* parameters.
* @param name the name attached to the OAuth 2.0 token
* @param name the name attached to the OAuth 2.0 token, may be {@code null}
* @param attributes the attributes of the OAuth 2.0 token
* @param authorities the authorities of the OAuth 2.0 token
* @param authorities the authorities of the OAuth 2.0 token, may be {@code null}
*/
public DefaultOAuth2AuthenticatedPrincipal(String name, Map<String, Object> attributes,
Collection<GrantedAuthority> authorities) {
public DefaultOAuth2AuthenticatedPrincipal(@Nullable String name, Map<String, Object> attributes,
@Nullable Collection<GrantedAuthority> authorities) {
Assert.notEmpty(attributes, "attributes cannot be empty");
this.attributes = Collections.unmodifiableMap(attributes);
this.authorities = (authorities != null) ? Collections.unmodifiableCollection(authorities)
: AuthorityUtils.NO_AUTHORITIES;
this.name = (name != null) ? name : (String) this.attributes.get("sub");
// Ensure name is never null - use 'sub' attribute as fallback, then empty string
// This satisfies AuthenticatedPrincipal.getName() contract which never returns
// null
String resolvedName = (name != null) ? name : (String) this.attributes.get("sub");
this.name = (resolvedName != null) ? resolvedName : "";
}
/**

View File

@ -22,6 +22,8 @@ import java.time.Instant;
import java.util.Collections;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
@ -52,11 +54,12 @@ public class OAuth2AccessToken extends AbstractOAuth2Token {
* Constructs an {@code OAuth2AccessToken} using the provided parameters.
* @param tokenType the token type
* @param tokenValue the token value
* @param issuedAt the time at which the token was issued
* @param issuedAt the time at which the token was issued, may be {@code null}
* @param expiresAt the expiration time on or after which the token MUST NOT be
* accepted
* accepted, may be {@code null}
*/
public OAuth2AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt) {
public OAuth2AccessToken(TokenType tokenType, String tokenValue, @Nullable Instant issuedAt,
@Nullable Instant expiresAt) {
this(tokenType, tokenValue, issuedAt, expiresAt, Collections.emptySet());
}
@ -64,13 +67,13 @@ public class OAuth2AccessToken extends AbstractOAuth2Token {
* Constructs an {@code OAuth2AccessToken} using the provided parameters.
* @param tokenType the token type
* @param tokenValue the token value
* @param issuedAt the time at which the token was issued
* @param issuedAt the time at which the token was issued, may be {@code null}
* @param expiresAt the expiration time on or after which the token MUST NOT be
* accepted
* accepted, may be {@code null}
* @param scopes the scope(s) associated to the token
*/
public OAuth2AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt,
Set<String> scopes) {
public OAuth2AccessToken(TokenType tokenType, String tokenValue, @Nullable Instant issuedAt,
@Nullable Instant expiresAt, Set<String> scopes) {
super(tokenValue, issuedAt, expiresAt);
Assert.notNull(tokenType, "tokenType cannot be null");
this.tokenType = tokenType;

View File

@ -19,7 +19,8 @@ package org.springframework.security.oauth2.core;
import java.util.Collection;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.GrantedAuthority;
@ -38,9 +39,8 @@ public interface OAuth2AuthenticatedPrincipal extends AuthenticatedPrincipal {
* @param <A> the type of the attribute
* @return the attribute or {@code null} otherwise
*/
@Nullable
@SuppressWarnings("unchecked")
default <A> A getAttribute(String name) {
default <A> @Nullable A getAttribute(String name) {
return (A) getAttributes().get(name);
}

View File

@ -18,6 +18,8 @@ package org.springframework.security.oauth2.core;
import java.io.Serial;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.util.Assert;
@ -77,22 +79,25 @@ public class OAuth2AuthenticationException extends AuthenticationException {
/**
* Constructs an {@code OAuth2AuthenticationException} using the provided parameters.
* @param error the {@link OAuth2Error OAuth 2.0 Error}
* @param message the detail message
* @param message the detail message, may be {@code null}
*/
public OAuth2AuthenticationException(OAuth2Error error, String message) {
public OAuth2AuthenticationException(OAuth2Error error, @Nullable String message) {
this(error, message, null);
}
/**
* Constructs an {@code OAuth2AuthenticationException} using the provided parameters.
* @param error the {@link OAuth2Error OAuth 2.0 Error}
* @param message the detail message
* @param cause the root cause
* @param message the detail message, may be {@code null}
* @param cause the root cause, may be {@code null}
*/
public OAuth2AuthenticationException(OAuth2Error error, String message, Throwable cause) {
super(message, cause);
public OAuth2AuthenticationException(OAuth2Error error, @Nullable String message, @Nullable Throwable cause) {
super(message);
Assert.notNull(error, "error cannot be null");
this.error = error;
if (cause != null) {
initCause(cause);
}
}
/**

View File

@ -18,6 +18,8 @@ package org.springframework.security.oauth2.core;
import java.io.Serializable;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
@ -41,9 +43,9 @@ public class OAuth2Error implements Serializable {
private final String errorCode;
private final String description;
private final @Nullable String description;
private final String uri;
private final @Nullable String uri;
/**
* Constructs an {@code OAuth2Error} using the provided parameters.
@ -56,10 +58,10 @@ public class OAuth2Error implements Serializable {
/**
* Constructs an {@code OAuth2Error} using the provided parameters.
* @param errorCode the error code
* @param description the error description
* @param uri the error uri
* @param description the error description, may be {@code null}
* @param uri the error uri, may be {@code null}
*/
public OAuth2Error(String errorCode, String description, String uri) {
public OAuth2Error(String errorCode, @Nullable String description, @Nullable String uri) {
Assert.hasText(errorCode, "errorCode cannot be empty");
this.errorCode = errorCode;
this.description = description;
@ -76,17 +78,17 @@ public class OAuth2Error implements Serializable {
/**
* Returns the error description.
* @return the error description
* @return the error description, or {@code null} if not available
*/
public final String getDescription() {
public final @Nullable String getDescription() {
return this.description;
}
/**
* Returns the error uri.
* @return the error uri
* @return the error uri, or {@code null} if not available
*/
public final String getUri() {
public final @Nullable String getUri() {
return this.uri;
}

View File

@ -19,6 +19,8 @@ package org.springframework.security.oauth2.core;
import java.io.Serial;
import java.time.Instant;
import org.jspecify.annotations.Nullable;
/**
* An implementation of an {@link AbstractOAuth2Token} representing an OAuth 2.0 Refresh
* Token.
@ -43,20 +45,20 @@ public class OAuth2RefreshToken extends AbstractOAuth2Token {
/**
* Constructs an {@code OAuth2RefreshToken} using the provided parameters.
* @param tokenValue the token value
* @param issuedAt the time at which the token was issued
* @param issuedAt the time at which the token was issued, may be {@code null}
*/
public OAuth2RefreshToken(String tokenValue, Instant issuedAt) {
public OAuth2RefreshToken(String tokenValue, @Nullable Instant issuedAt) {
this(tokenValue, issuedAt, null);
}
/**
* Constructs an {@code OAuth2RefreshToken} using the provided parameters.
* @param tokenValue the token value
* @param issuedAt the time at which the token was issued
* @param expiresAt the time at which the token expires
* @param issuedAt the time at which the token was issued, may be {@code null}
* @param expiresAt the time at which the token expires, may be {@code null}
* @since 5.5
*/
public OAuth2RefreshToken(String tokenValue, Instant issuedAt, Instant expiresAt) {
public OAuth2RefreshToken(String tokenValue, @Nullable Instant issuedAt, @Nullable Instant expiresAt) {
super(tokenValue, issuedAt, expiresAt);
}

View File

@ -18,7 +18,7 @@ package org.springframework.security.oauth2.core;
import java.time.Instant;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
/**
* Core interface representing an OAuth 2.0 Token.
@ -39,8 +39,7 @@ public interface OAuth2Token {
* Returns the time at which the token was issued.
* @return the time the token was issued or {@code null}
*/
@Nullable
default Instant getIssuedAt() {
default @Nullable Instant getIssuedAt() {
return null;
}
@ -48,8 +47,7 @@ public interface OAuth2Token {
* Returns the expiration time on or after which the token MUST NOT be accepted.
* @return the token expiration time or {@code null}
*/
@Nullable
default Instant getExpiresAt() {
default @Nullable Instant getExpiresAt() {
return null;
}

View File

@ -20,7 +20,7 @@ import java.net.URL;
import java.time.Instant;
import java.util.List;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
/**
* A {@link ClaimAccessor} for the &quot;claims&quot; that may be contained in the
@ -45,105 +45,106 @@ public interface OAuth2TokenIntrospectionClaimAccessor extends ClaimAccessor {
/**
* Returns a human-readable identifier {@code (username)} for the resource owner that
* authorized the token
* authorized the token, or {@code null} if it does not exist.
* @return a human-readable identifier for the resource owner that authorized the
* token
* token, or {@code null} if it does not exist
*/
@Nullable
default String getUsername() {
default @Nullable String getUsername() {
return getClaimAsString(OAuth2TokenIntrospectionClaimNames.USERNAME);
}
/**
* Returns the client identifier {@code (client_id)} for the token
* @return the client identifier for the token
* Returns the client identifier {@code (client_id)} for the token, or {@code null} if
* it does not exist.
* @return the client identifier for the token, or {@code null} if it does not exist
*/
@Nullable
default String getClientId() {
default @Nullable String getClientId() {
return getClaimAsString(OAuth2TokenIntrospectionClaimNames.CLIENT_ID);
}
/**
* Returns the scopes {@code (scope)} associated with the token
* @return the scopes associated with the token
* Returns the scopes {@code (scope)} associated with the token, or {@code null} if it
* does not exist.
* @return the scopes associated with the token, or {@code null} if it does not exist
*/
@Nullable
default List<String> getScopes() {
default @Nullable List<String> getScopes() {
return getClaimAsStringList(OAuth2TokenIntrospectionClaimNames.SCOPE);
}
/**
* Returns the type of the token {@code (token_type)}, for example {@code bearer}.
* @return the type of the token, for example {@code bearer}.
* Returns the type of the token {@code (token_type)}, for example {@code bearer}, or
* {@code null} if it does not exist.
* @return the type of the token, for example {@code bearer}, or {@code null} if it
* does not exist
*/
@Nullable
default String getTokenType() {
default @Nullable String getTokenType() {
return getClaimAsString(OAuth2TokenIntrospectionClaimNames.TOKEN_TYPE);
}
/**
* Returns a timestamp {@code (exp)} indicating when the token expires
* @return a timestamp indicating when the token expires
* Returns a timestamp {@code (exp)} indicating when the token expires, or
* {@code null} if it does not exist.
* @return a timestamp indicating when the token expires, or {@code null} if it does
* not exist
*/
@Nullable
default Instant getExpiresAt() {
default @Nullable Instant getExpiresAt() {
return getClaimAsInstant(OAuth2TokenIntrospectionClaimNames.EXP);
}
/**
* Returns a timestamp {@code (iat)} indicating when the token was issued
* @return a timestamp indicating when the token was issued
* Returns a timestamp {@code (iat)} indicating when the token was issued, or
* {@code null} if it does not exist.
* @return a timestamp indicating when the token was issued, or {@code null} if it
* does not exist
*/
@Nullable
default Instant getIssuedAt() {
default @Nullable Instant getIssuedAt() {
return getClaimAsInstant(OAuth2TokenIntrospectionClaimNames.IAT);
}
/**
* Returns a timestamp {@code (nbf)} indicating when the token is not to be used
* before
* @return a timestamp indicating when the token is not to be used before
* before, or {@code null} if it does not exist.
* @return a timestamp indicating when the token is not to be used before, or
* {@code null} if it does not exist
*/
@Nullable
default Instant getNotBefore() {
default @Nullable Instant getNotBefore() {
return getClaimAsInstant(OAuth2TokenIntrospectionClaimNames.NBF);
}
/**
* Returns usually a machine-readable identifier {@code (sub)} of the resource owner
* who authorized the token
* who authorized the token, or {@code null} if it does not exist.
* @return usually a machine-readable identifier of the resource owner who authorized
* the token
* the token, or {@code null} if it does not exist
*/
@Nullable
default String getSubject() {
default @Nullable String getSubject() {
return getClaimAsString(OAuth2TokenIntrospectionClaimNames.SUB);
}
/**
* Returns the intended audience {@code (aud)} for the token
* @return the intended audience for the token
* Returns the intended audience {@code (aud)} for the token, or {@code null} if it
* does not exist.
* @return the intended audience for the token, or {@code null} if it does not exist
*/
@Nullable
default List<String> getAudience() {
default @Nullable List<String> getAudience() {
return getClaimAsStringList(OAuth2TokenIntrospectionClaimNames.AUD);
}
/**
* Returns the issuer {@code (iss)} of the token
* @return the issuer of the token
* Returns the issuer {@code (iss)} of the token, or {@code null} if it does not
* exist.
* @return the issuer of the token, or {@code null} if it does not exist
*/
@Nullable
default URL getIssuer() {
default @Nullable URL getIssuer() {
return getClaimAsURL(OAuth2TokenIntrospectionClaimNames.ISS);
}
/**
* Returns the identifier {@code (jti)} for the token
* @return the identifier for the token
* Returns the identifier {@code (jti)} for the token, or {@code null} if it does not
* exist.
* @return the identifier for the token, or {@code null} if it does not exist
*/
@Nullable
default String getId() {
default @Nullable String getId() {
return getClaimAsString(OAuth2TokenIntrospectionClaimNames.JTI);
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* Support classes that provide OAuth 2.0 authorization managers.
*/
@NullMarked
package org.springframework.security.oauth2.core.authorization;
import org.jspecify.annotations.NullMarked;

View File

@ -16,6 +16,8 @@
package org.springframework.security.oauth2.core.converter;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.support.GenericConversionService;
@ -32,7 +34,7 @@ import org.springframework.security.oauth2.core.ClaimAccessor;
*/
public final class ClaimConversionService extends GenericConversionService {
private static volatile ClaimConversionService sharedInstance;
private static volatile @Nullable ClaimConversionService sharedInstance;
private ClaimConversionService() {
addConverters(this);

View File

@ -19,6 +19,8 @@ package org.springframework.security.oauth2.core.converter;
import java.util.Collections;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
@ -34,7 +36,7 @@ final class ObjectToBooleanConverter implements GenericConverter {
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}

View File

@ -21,6 +21,8 @@ import java.util.Collections;
import java.util.Date;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
@ -36,7 +38,7 @@ final class ObjectToInstantConverter implements GenericConverter {
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}

View File

@ -23,6 +23,8 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.util.ClassUtils;
@ -49,7 +51,7 @@ final class ObjectToListStringConverter implements ConditionalGenericConverter {
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}

View File

@ -21,6 +21,8 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
@ -37,12 +39,13 @@ final class ObjectToMapStringObjectConverter implements ConditionalGenericConver
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
TypeDescriptor mapKeyTypeDescriptor = targetType.getMapKeyTypeDescriptor();
return targetType.getElementTypeDescriptor() == null
|| targetType.getMapKeyTypeDescriptor().getType().equals(String.class);
|| (mapKeyTypeDescriptor != null && mapKeyTypeDescriptor.getType().equals(String.class));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}

View File

@ -19,6 +19,8 @@ package org.springframework.security.oauth2.core.converter;
import java.util.Collections;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
@ -34,7 +36,7 @@ final class ObjectToStringConverter implements GenericConverter {
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return (source != null) ? source.toString() : null;
}

View File

@ -21,6 +21,8 @@ import java.net.URL;
import java.util.Collections;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
@ -36,7 +38,7 @@ final class ObjectToURLConverter implements GenericConverter {
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* Support classes that provide claim type converters for OAuth 2.0 and OpenID Connect.
*/
@NullMarked
package org.springframework.security.oauth2.core.converter;
import org.jspecify.annotations.NullMarked;

View File

@ -23,6 +23,8 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.util.StringUtils;
@ -44,6 +46,9 @@ public final class DefaultMapOAuth2AccessTokenResponseConverter
@Override
public OAuth2AccessTokenResponse convert(Map<String, Object> source) {
String accessToken = getParameterValue(source, OAuth2ParameterNames.ACCESS_TOKEN);
if (accessToken == null) {
throw new IllegalArgumentException("Missing required parameter: " + OAuth2ParameterNames.ACCESS_TOKEN);
}
OAuth2AccessToken.TokenType accessTokenType = getAccessTokenType(source);
long expiresIn = getExpiresIn(source);
Set<String> scopes = getScopes(source);
@ -65,7 +70,8 @@ public final class DefaultMapOAuth2AccessTokenResponseConverter
// @formatter:on
}
private static OAuth2AccessToken.TokenType getAccessTokenType(Map<String, Object> tokenResponseParameters) {
private static OAuth2AccessToken.@Nullable TokenType getAccessTokenType(
Map<String, Object> tokenResponseParameters) {
if (OAuth2AccessToken.TokenType.BEARER.getValue()
.equalsIgnoreCase(getParameterValue(tokenResponseParameters, OAuth2ParameterNames.TOKEN_TYPE))) {
return OAuth2AccessToken.TokenType.BEARER;
@ -89,7 +95,8 @@ public final class DefaultMapOAuth2AccessTokenResponseConverter
return Collections.emptySet();
}
private static String getParameterValue(Map<String, Object> tokenResponseParameters, String parameterName) {
private static @Nullable String getParameterValue(Map<String, Object> tokenResponseParameters,
String parameterName) {
Object obj = tokenResponseParameters.get(parameterName);
return (obj != null) ? obj.toString() : null;
}

View File

@ -21,9 +21,11 @@ import java.util.Collections;
import java.util.Map;
import java.util.Set;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
@ -39,11 +41,11 @@ import org.springframework.util.StringUtils;
*/
public final class OAuth2AccessTokenResponse {
private OAuth2AccessToken accessToken;
private @Nullable OAuth2AccessToken accessToken;
private OAuth2RefreshToken refreshToken;
private @Nullable OAuth2RefreshToken refreshToken;
private Map<String, Object> additionalParameters;
private @Nullable Map<String, Object> additionalParameters;
private OAuth2AccessTokenResponse() {
}
@ -53,12 +55,14 @@ public final class OAuth2AccessTokenResponse {
* @return the {@link OAuth2AccessToken}
*/
public OAuth2AccessToken getAccessToken() {
Assert.notNull(this.accessToken, "accessToken cannot be null");
return this.accessToken;
}
/**
* Returns the {@link OAuth2RefreshToken Refresh Token}.
* @return the {@link OAuth2RefreshToken}
* @return the {@link OAuth2RefreshToken}, or {@code null} if not present in the
* response
* @since 5.1
*/
public @Nullable OAuth2RefreshToken getRefreshToken() {
@ -71,6 +75,7 @@ public final class OAuth2AccessTokenResponse {
* empty.
*/
public Map<String, Object> getAdditionalParameters() {
Assert.notNull(this.additionalParameters, "additionalParameters cannot be null");
return this.additionalParameters;
}
@ -99,19 +104,19 @@ public final class OAuth2AccessTokenResponse {
private String tokenValue;
private OAuth2AccessToken.TokenType tokenType;
private OAuth2AccessToken.@Nullable TokenType tokenType;
private Instant issuedAt;
private @Nullable Instant issuedAt;
private Instant expiresAt;
private @Nullable Instant expiresAt;
private long expiresIn;
private Set<String> scopes;
private @Nullable Set<String> scopes;
private String refreshToken;
private @Nullable String refreshToken;
private Map<String, Object> additionalParameters;
private @Nullable Map<String, Object> additionalParameters;
private Builder(OAuth2AccessTokenResponse response) {
OAuth2AccessToken accessToken = response.getAccessToken();
@ -131,10 +136,10 @@ public final class OAuth2AccessTokenResponse {
/**
* Sets the {@link OAuth2AccessToken.TokenType token type}.
* @param tokenType the type of token issued
* @param tokenType the type of token issued, may be {@code null}
* @return the {@link Builder}
*/
public Builder tokenType(OAuth2AccessToken.TokenType tokenType) {
public Builder tokenType(OAuth2AccessToken.@Nullable TokenType tokenType) {
this.tokenType = tokenType;
return this;
}
@ -152,30 +157,32 @@ public final class OAuth2AccessTokenResponse {
/**
* Sets the scope(s) associated to the access token.
* @param scopes the scope(s) associated to the access token.
* @param scopes the scope(s) associated to the access token, may be {@code null}
* @return the {@link Builder}
*/
public Builder scopes(Set<String> scopes) {
public Builder scopes(@Nullable Set<String> scopes) {
this.scopes = scopes;
return this;
}
/**
* Sets the refresh token associated to the access token.
* @param refreshToken the refresh token associated to the access token.
* @param refreshToken the refresh token associated to the access token, may be
* {@code null}
* @return the {@link Builder}
*/
public Builder refreshToken(String refreshToken) {
public Builder refreshToken(@Nullable String refreshToken) {
this.refreshToken = refreshToken;
return this;
}
/**
* Sets the additional parameters returned in the response.
* @param additionalParameters the additional parameters returned in the response
* @param additionalParameters the additional parameters returned in the response,
* may be {@code null}
* @return the {@link Builder}
*/
public Builder additionalParameters(Map<String, Object> additionalParameters) {
public Builder additionalParameters(@Nullable Map<String, Object> additionalParameters) {
this.additionalParameters = additionalParameters;
return this;
}
@ -185,11 +192,14 @@ public final class OAuth2AccessTokenResponse {
* @return a {@link OAuth2AccessTokenResponse}
*/
public OAuth2AccessTokenResponse build() {
Assert.notNull(this.tokenType, "tokenType cannot be null");
Instant issuedAt = getIssuedAt();
Instant expiresAt = getExpiresAt();
// Convert nullable scopes to non-null for constructor
Set<String> scopesToUse = (this.scopes != null) ? this.scopes : Collections.emptySet();
OAuth2AccessTokenResponse accessTokenResponse = new OAuth2AccessTokenResponse();
accessTokenResponse.accessToken = new OAuth2AccessToken(this.tokenType, this.tokenValue, issuedAt,
expiresAt, this.scopes);
expiresAt, scopesToUse);
if (StringUtils.hasText(this.refreshToken)) {
accessTokenResponse.refreshToken = new OAuth2RefreshToken(this.refreshToken, issuedAt);
}

View File

@ -30,6 +30,8 @@ import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@ -65,11 +67,11 @@ public class OAuth2AuthorizationRequest implements Serializable {
private final String clientId;
private final String redirectUri;
private final @Nullable String redirectUri;
private final Set<String> scopes;
private final String state;
private final @Nullable String state;
private final Map<String, Object> additionalParameters;
@ -80,6 +82,8 @@ public class OAuth2AuthorizationRequest implements Serializable {
protected OAuth2AuthorizationRequest(AbstractBuilder<?, ?> builder) {
Assert.hasText(builder.authorizationUri, "authorizationUri cannot be empty");
Assert.hasText(builder.clientId, "clientId cannot be empty");
Assert.notNull(builder.authorizationUri, "authorizationUri cannot be null");
Assert.notNull(builder.clientId, "clientId cannot be null");
this.authorizationUri = builder.authorizationUri;
this.authorizationGrantType = builder.authorizationGrantType;
this.responseType = builder.responseType;
@ -89,8 +93,9 @@ public class OAuth2AuthorizationRequest implements Serializable {
CollectionUtils.isEmpty(builder.scopes) ? Collections.emptySet() : new LinkedHashSet<>(builder.scopes));
this.state = builder.state;
this.additionalParameters = Collections.unmodifiableMap(builder.additionalParameters);
this.authorizationRequestUri = StringUtils.hasText(builder.authorizationRequestUri)
? builder.authorizationRequestUri : builder.buildAuthorizationRequestUri();
String builderUri = builder.authorizationRequestUri;
this.authorizationRequestUri = StringUtils.hasText(builderUri) ? builderUri
: builder.buildAuthorizationRequestUri();
this.attributes = Collections.unmodifiableMap(builder.attributes);
}
@ -127,10 +132,10 @@ public class OAuth2AuthorizationRequest implements Serializable {
}
/**
* Returns the uri for the redirection endpoint.
* @return the uri for the redirection endpoint
* Returns the uri for the redirection endpoint, or {@code null} if not present.
* @return the uri for the redirection endpoint, or {@code null}
*/
public String getRedirectUri() {
public @Nullable String getRedirectUri() {
return this.redirectUri;
}
@ -143,10 +148,10 @@ public class OAuth2AuthorizationRequest implements Serializable {
}
/**
* Returns the state.
* @return the state
* Returns the state, or {@code null} if not present.
* @return the state, or {@code null}
*/
public String getState() {
public @Nullable String getState() {
return this.state;
}
@ -177,7 +182,7 @@ public class OAuth2AuthorizationRequest implements Serializable {
* @since 5.2
*/
@SuppressWarnings("unchecked")
public <T> T getAttribute(String name) {
public <T> @Nullable T getAttribute(String name) {
return (T) this.getAttributes().get(name);
}
@ -277,19 +282,19 @@ public class OAuth2AuthorizationRequest implements Serializable {
*/
protected abstract static class AbstractBuilder<T extends OAuth2AuthorizationRequest, B extends AbstractBuilder<T, B>> {
private String authorizationUri;
private @Nullable String authorizationUri;
private final AuthorizationGrantType authorizationGrantType = AuthorizationGrantType.AUTHORIZATION_CODE;
private final OAuth2AuthorizationResponseType responseType = OAuth2AuthorizationResponseType.CODE;
private String clientId;
private @Nullable String clientId;
private String redirectUri;
private @Nullable String redirectUri;
private Set<String> scopes;
private @Nullable Set<String> scopes;
private String state;
private @Nullable String state;
private Map<String, Object> additionalParameters = new LinkedHashMap<>();
@ -298,7 +303,7 @@ public class OAuth2AuthorizationRequest implements Serializable {
private Map<String, Object> attributes = new LinkedHashMap<>();
private String authorizationRequestUri;
private @Nullable String authorizationRequestUri;
private Function<UriBuilder, URI> authorizationRequestUriFunction = (builder) -> builder.build();
@ -341,20 +346,20 @@ public class OAuth2AuthorizationRequest implements Serializable {
/**
* Sets the uri for the redirection endpoint.
* @param redirectUri the uri for the redirection endpoint
* @param redirectUri the uri for the redirection endpoint, may be {@code null}
* @return the {@link AbstractBuilder}
*/
public B redirectUri(String redirectUri) {
public B redirectUri(@Nullable String redirectUri) {
this.redirectUri = redirectUri;
return getThis();
}
/**
* Sets the scope(s).
* @param scope the scope(s)
* @param scope the scope(s), may be {@code null}
* @return the {@link AbstractBuilder}
*/
public B scope(String... scope) {
public B scope(@Nullable String... scope) {
if (scope != null && scope.length > 0) {
return scopes(new LinkedHashSet<>(Arrays.asList(scope)));
}
@ -363,20 +368,20 @@ public class OAuth2AuthorizationRequest implements Serializable {
/**
* Sets the scope(s).
* @param scopes the scope(s)
* @param scopes the scope(s), may be {@code null}
* @return the {@link AbstractBuilder}
*/
public B scopes(Set<String> scopes) {
public B scopes(@Nullable Set<String> scopes) {
this.scopes = scopes;
return getThis();
}
/**
* Sets the state.
* @param state the state
* @param state the state, may be {@code null}
* @return the {@link AbstractBuilder}
*/
public B state(String state) {
public B state(@Nullable String state) {
this.state = state;
return getThis();
}
@ -502,7 +507,9 @@ public class OAuth2AuthorizationRequest implements Serializable {
queryParams.set(key, encodeQueryParam(String.valueOf(v)));
}
});
UriBuilder uriBuilder = this.uriBuilderFactory.uriString(this.authorizationUri).queryParams(queryParams);
String uri = this.authorizationUri;
Assert.notNull(uri, "authorizationUri cannot be null");
UriBuilder uriBuilder = this.uriBuilderFactory.uriString(uri).queryParams(queryParams);
return this.authorizationRequestUriFunction.apply(uriBuilder).toString();
}

View File

@ -19,6 +19,8 @@ package org.springframework.security.oauth2.core.endpoint;
import java.io.Serial;
import java.io.Serializable;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@ -39,13 +41,13 @@ public final class OAuth2AuthorizationResponse implements Serializable {
@Serial
private static final long serialVersionUID = 620L;
private String redirectUri;
private @Nullable String redirectUri;
private String state;
private @Nullable String state;
private String code;
private @Nullable String code;
private OAuth2Error error;
private @Nullable OAuth2Error error;
private OAuth2AuthorizationResponse() {
}
@ -55,22 +57,24 @@ public final class OAuth2AuthorizationResponse implements Serializable {
* @return the uri where the response was redirected to
*/
public String getRedirectUri() {
Assert.notNull(this.redirectUri, "redirectUri cannot be null");
return this.redirectUri;
}
/**
* Returns the state.
* @return the state
* Returns the state, or {@code null} if not present.
* @return the state, or {@code null}
*/
public String getState() {
public @Nullable String getState() {
return this.state;
}
/**
* Returns the authorization code.
* @return the authorization code
* Returns the authorization code, or {@code null} if the response is an error
* response.
* @return the authorization code, or {@code null}
*/
public String getCode() {
public @Nullable String getCode() {
return this.code;
}
@ -80,7 +84,7 @@ public final class OAuth2AuthorizationResponse implements Serializable {
* @return the {@link OAuth2Error} if the Authorization Request failed, otherwise
* {@code null}
*/
public OAuth2Error getError() {
public @Nullable OAuth2Error getError() {
return this.error;
}
@ -127,17 +131,17 @@ public final class OAuth2AuthorizationResponse implements Serializable {
*/
public static final class Builder {
private String redirectUri;
private @Nullable String redirectUri;
private String state;
private @Nullable String state;
private String code;
private @Nullable String code;
private String errorCode;
private @Nullable String errorCode;
private String errorDescription;
private @Nullable String errorDescription;
private String errorUri;
private @Nullable String errorUri;
private Builder() {
}
@ -218,6 +222,7 @@ public final class OAuth2AuthorizationResponse implements Serializable {
authorizationResponse.code = this.code;
}
else {
Assert.hasText(this.errorCode, "errorCode cannot be empty when code is not present");
authorizationResponse.error = new OAuth2Error(this.errorCode, this.errorDescription, this.errorUri);
}
return authorizationResponse;

View File

@ -21,6 +21,8 @@ import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.OAuth2DeviceCode;
import org.springframework.security.oauth2.core.OAuth2UserCode;
import org.springframework.util.Assert;
@ -38,17 +40,17 @@ import org.springframework.util.CollectionUtils;
*/
public final class OAuth2DeviceAuthorizationResponse {
private OAuth2DeviceCode deviceCode;
private @Nullable OAuth2DeviceCode deviceCode;
private OAuth2UserCode userCode;
private @Nullable OAuth2UserCode userCode;
private String verificationUri;
private @Nullable String verificationUri;
private String verificationUriComplete;
private @Nullable String verificationUriComplete;
private long interval;
private Map<String, Object> additionalParameters;
private @Nullable Map<String, Object> additionalParameters;
private OAuth2DeviceAuthorizationResponse() {
}
@ -58,6 +60,7 @@ public final class OAuth2DeviceAuthorizationResponse {
* @return the {@link OAuth2DeviceCode}
*/
public OAuth2DeviceCode getDeviceCode() {
Assert.notNull(this.deviceCode, "deviceCode cannot be null");
return this.deviceCode;
}
@ -66,6 +69,7 @@ public final class OAuth2DeviceAuthorizationResponse {
* @return the {@link OAuth2UserCode}
*/
public OAuth2UserCode getUserCode() {
Assert.notNull(this.userCode, "userCode cannot be null");
return this.userCode;
}
@ -74,14 +78,16 @@ public final class OAuth2DeviceAuthorizationResponse {
* @return the end-user verification URI
*/
public String getVerificationUri() {
Assert.notNull(this.verificationUri, "verificationUri cannot be null");
return this.verificationUri;
}
/**
* Returns the end-user verification URI that includes the user code.
* @return the end-user verification URI that includes the user code
* Returns the end-user verification URI that includes the user code, or {@code null}
* if not present.
* @return the end-user verification URI that includes the user code, or {@code null}
*/
public String getVerificationUriComplete() {
public @Nullable String getVerificationUriComplete() {
return this.verificationUriComplete;
}
@ -100,6 +106,7 @@ public final class OAuth2DeviceAuthorizationResponse {
* empty.
*/
public Map<String, Object> getAdditionalParameters() {
Assert.notNull(this.additionalParameters, "additionalParameters cannot be null");
return this.additionalParameters;
}
@ -138,15 +145,15 @@ public final class OAuth2DeviceAuthorizationResponse {
private final String userCode;
private String verificationUri;
private @Nullable String verificationUri;
private String verificationUriComplete;
private @Nullable String verificationUriComplete;
private long expiresIn;
private long interval;
private Map<String, Object> additionalParameters;
private @Nullable Map<String, Object> additionalParameters;
private Builder(OAuth2DeviceCode deviceCode, OAuth2UserCode userCode) {
this.deviceCode = deviceCode.getTokenValue();
@ -172,10 +179,10 @@ public final class OAuth2DeviceAuthorizationResponse {
/**
* Sets the end-user verification URI that includes the user code.
* @param verificationUriComplete the end-user verification URI that includes the
* user code
* user code, may be {@code null}
* @return the {@link Builder}
*/
public Builder verificationUriComplete(String verificationUriComplete) {
public Builder verificationUriComplete(@Nullable String verificationUriComplete) {
this.verificationUriComplete = verificationUriComplete;
return this;
}

View File

@ -18,4 +18,7 @@
* Support classes that model the OAuth 2.0 Request and Response messages from the
* Authorization Endpoint and Token Endpoint.
*/
@NullMarked
package org.springframework.security.oauth2.core.endpoint;
import org.jspecify.annotations.NullMarked;

View File

@ -16,6 +16,8 @@
package org.springframework.security.oauth2.core.http.converter;
import org.jspecify.annotations.Nullable;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
@ -54,7 +56,7 @@ final class HttpMessageConverters {
}
@SuppressWarnings("removal")
static GenericHttpMessageConverter<Object> getJsonMessageConverter() {
static @Nullable GenericHttpMessageConverter<Object> getJsonMessageConverter() {
if (jacksonPresent) {
return new GenericHttpMessageConverterAdapter<>(new JacksonJsonHttpMessageConverter());
}

View File

@ -52,8 +52,7 @@ public class OAuth2AccessTokenResponseHttpMessageConverter
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
};
private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters
.getJsonMessageConverter();
private final GenericHttpMessageConverter<Object> jsonMessageConverter;
private Converter<Map<String, Object>, OAuth2AccessTokenResponse> accessTokenResponseConverter = new DefaultMapOAuth2AccessTokenResponseConverter();
@ -61,6 +60,9 @@ public class OAuth2AccessTokenResponseHttpMessageConverter
public OAuth2AccessTokenResponseHttpMessageConverter() {
super(DEFAULT_CHARSET, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
GenericHttpMessageConverter<Object> converter = HttpMessageConverters.getJsonMessageConverter();
Assert.notNull(converter, "Unable to locate a supported JSON message converter");
this.jsonMessageConverter = converter;
}
@Override

View File

@ -25,6 +25,8 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpInputMessage;
@ -56,13 +58,18 @@ public class OAuth2DeviceAuthorizationResponseHttpMessageConverter
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
};
private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters
.getJsonMessageConverter();
private final GenericHttpMessageConverter<Object> jsonMessageConverter;
private Converter<Map<String, Object>, OAuth2DeviceAuthorizationResponse> deviceAuthorizationResponseConverter = new DefaultMapOAuth2DeviceAuthorizationResponseConverter();
private Converter<OAuth2DeviceAuthorizationResponse, Map<String, Object>> deviceAuthorizationResponseParametersConverter = new DefaultOAuth2DeviceAuthorizationResponseMapConverter();
public OAuth2DeviceAuthorizationResponseHttpMessageConverter() {
GenericHttpMessageConverter<Object> converter = HttpMessageConverters.getJsonMessageConverter();
Assert.notNull(converter, "Unable to locate a supported JSON message converter");
this.jsonMessageConverter = converter;
}
@Override
protected boolean supports(Class<?> clazz) {
return OAuth2DeviceAuthorizationResponse.class.isAssignableFrom(clazz);
@ -139,8 +146,18 @@ public class OAuth2DeviceAuthorizationResponseHttpMessageConverter
@Override
public OAuth2DeviceAuthorizationResponse convert(Map<String, Object> parameters) {
String deviceCode = getParameterValue(parameters, OAuth2ParameterNames.DEVICE_CODE);
if (deviceCode == null) {
throw new IllegalArgumentException("Missing required parameter: " + OAuth2ParameterNames.DEVICE_CODE);
}
String userCode = getParameterValue(parameters, OAuth2ParameterNames.USER_CODE);
if (userCode == null) {
throw new IllegalArgumentException("Missing required parameter: " + OAuth2ParameterNames.USER_CODE);
}
String verificationUri = getParameterValue(parameters, OAuth2ParameterNames.VERIFICATION_URI);
if (verificationUri == null) {
throw new IllegalArgumentException(
"Missing required parameter: " + OAuth2ParameterNames.VERIFICATION_URI);
}
String verificationUriComplete = getParameterValue(parameters,
OAuth2ParameterNames.VERIFICATION_URI_COMPLETE);
long expiresIn = getParameterValue(parameters, OAuth2ParameterNames.EXPIRES_IN, 0L);
@ -162,7 +179,7 @@ public class OAuth2DeviceAuthorizationResponseHttpMessageConverter
// @formatter:on
}
private static String getParameterValue(Map<String, Object> parameters, String parameterName) {
private static @Nullable String getParameterValue(Map<String, Object> parameters, String parameterName) {
Object obj = parameters.get(parameterName);
return (obj != null) ? obj.toString() : null;
}

View File

@ -52,8 +52,7 @@ public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverte
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
};
private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters
.getJsonMessageConverter();
private final GenericHttpMessageConverter<Object> jsonMessageConverter;
protected Converter<Map<String, String>, OAuth2Error> errorConverter = new OAuth2ErrorConverter();
@ -61,6 +60,9 @@ public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverte
public OAuth2ErrorHttpMessageConverter() {
super(DEFAULT_CHARSET, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
GenericHttpMessageConverter<Object> converter = HttpMessageConverters.getJsonMessageConverter();
Assert.notNull(converter, "Unable to locate a supported JSON message converter");
this.jsonMessageConverter = converter;
}
@Override
@ -133,6 +135,7 @@ public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverte
@Override
public OAuth2Error convert(Map<String, String> parameters) {
String errorCode = parameters.get(OAuth2ParameterNames.ERROR);
Assert.hasText(errorCode, "errorCode cannot be empty");
String errorDescription = parameters.get(OAuth2ParameterNames.ERROR_DESCRIPTION);
String errorUri = parameters.get(OAuth2ParameterNames.ERROR_URI);
return new OAuth2Error(errorCode, errorDescription, errorUri);

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* HTTP message converters for OAuth 2.0 and OpenID Connect protocol messages.
*/
@NullMarked
package org.springframework.security.oauth2.core.http.converter;
import org.jspecify.annotations.NullMarked;

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* Support classes that provide HTTP message conversion for OAuth 2.0 and OpenID Connect.
*/
@NullMarked
package org.springframework.security.oauth2.core.http;
import org.jspecify.annotations.NullMarked;

View File

@ -16,6 +16,8 @@
package org.springframework.security.oauth2.core.oidc;
import org.jspecify.annotations.Nullable;
/**
* The Address Claim represents a physical mailing address defined by the OpenID Connect
* Core 1.0 specification that can be returned either in the UserInfo Response or the ID
@ -34,40 +36,43 @@ package org.springframework.security.oauth2.core.oidc;
public interface AddressStandardClaim {
/**
* Returns the full mailing address, formatted for display.
* @return the full mailing address
* Returns the full mailing address, formatted for display, or {@code null} if it does
* not exist.
* @return the full mailing address, or {@code null} if it does not exist
*/
String getFormatted();
@Nullable String getFormatted();
/**
* Returns the full street address, which may include house number, street name, P.O.
* Box, etc.
* @return the full street address
* Box, etc., or {@code null} if it does not exist.
* @return the full street address, or {@code null} if it does not exist
*/
String getStreetAddress();
@Nullable String getStreetAddress();
/**
* Returns the city or locality.
* @return the city or locality
* Returns the city or locality, or {@code null} if it does not exist.
* @return the city or locality, or {@code null} if it does not exist
*/
String getLocality();
@Nullable String getLocality();
/**
* Returns the state, province, prefecture, or region.
* @return the state, province, prefecture, or region
* Returns the state, province, prefecture, or region, or {@code null} if it does not
* exist.
* @return the state, province, prefecture, or region, or {@code null} if it does not
* exist
*/
String getRegion();
@Nullable String getRegion();
/**
* Returns the zip code or postal code.
* @return the zip code or postal code
* Returns the zip code or postal code, or {@code null} if it does not exist.
* @return the zip code or postal code, or {@code null} if it does not exist
*/
String getPostalCode();
@Nullable String getPostalCode();
/**
* Returns the country.
* @return the country
* Returns the country, or {@code null} if it does not exist.
* @return the country, or {@code null} if it does not exist
*/
String getCountry();
@Nullable String getCountry();
}

View File

@ -18,6 +18,8 @@ package org.springframework.security.oauth2.core.oidc;
import java.util.Map;
import org.jspecify.annotations.Nullable;
/**
* The default implementation of an {@link AddressStandardClaim Address Claim}.
*
@ -27,48 +29,48 @@ import java.util.Map;
*/
public final class DefaultAddressStandardClaim implements AddressStandardClaim {
private String formatted;
private @Nullable String formatted;
private String streetAddress;
private @Nullable String streetAddress;
private String locality;
private @Nullable String locality;
private String region;
private @Nullable String region;
private String postalCode;
private @Nullable String postalCode;
private String country;
private @Nullable String country;
private DefaultAddressStandardClaim() {
}
@Override
public String getFormatted() {
public @Nullable String getFormatted() {
return this.formatted;
}
@Override
public String getStreetAddress() {
public @Nullable String getStreetAddress() {
return this.streetAddress;
}
@Override
public String getLocality() {
public @Nullable String getLocality() {
return this.locality;
}
@Override
public String getRegion() {
public @Nullable String getRegion() {
return this.region;
}
@Override
public String getPostalCode() {
public @Nullable String getPostalCode() {
return this.postalCode;
}
@Override
public String getCountry() {
public @Nullable String getCountry() {
return this.country;
}
@ -131,17 +133,17 @@ public final class DefaultAddressStandardClaim implements AddressStandardClaim {
private static final String COUNTRY_FIELD_NAME = "country";
private String formatted;
private @Nullable String formatted;
private String streetAddress;
private @Nullable String streetAddress;
private String locality;
private @Nullable String locality;
private String region;
private @Nullable String region;
private String postalCode;
private @Nullable String postalCode;
private String country;
private @Nullable String country;
/**
* Default constructor.
@ -165,10 +167,10 @@ public final class DefaultAddressStandardClaim implements AddressStandardClaim {
/**
* Sets the full mailing address, formatted for display.
* @param formatted the full mailing address
* @param formatted the full mailing address, may be {@code null}
* @return the {@link Builder}
*/
public Builder formatted(String formatted) {
public Builder formatted(@Nullable String formatted) {
this.formatted = formatted;
return this;
}
@ -176,50 +178,50 @@ public final class DefaultAddressStandardClaim implements AddressStandardClaim {
/**
* Sets the full street address, which may include house number, street name, P.O.
* Box, etc.
* @param streetAddress the full street address
* @param streetAddress the full street address, may be {@code null}
* @return the {@link Builder}
*/
public Builder streetAddress(String streetAddress) {
public Builder streetAddress(@Nullable String streetAddress) {
this.streetAddress = streetAddress;
return this;
}
/**
* Sets the city or locality.
* @param locality the city or locality
* @param locality the city or locality, may be {@code null}
* @return the {@link Builder}
*/
public Builder locality(String locality) {
public Builder locality(@Nullable String locality) {
this.locality = locality;
return this;
}
/**
* Sets the state, province, prefecture, or region.
* @param region the state, province, prefecture, or region
* @param region the state, province, prefecture, or region, may be {@code null}
* @return the {@link Builder}
*/
public Builder region(String region) {
public Builder region(@Nullable String region) {
this.region = region;
return this;
}
/**
* Sets the zip code or postal code.
* @param postalCode the zip code or postal code
* @param postalCode the zip code or postal code, may be {@code null}
* @return the {@link Builder}
*/
public Builder postalCode(String postalCode) {
public Builder postalCode(@Nullable String postalCode) {
this.postalCode = postalCode;
return this;
}
/**
* Sets the country.
* @param country the country
* @param country the country, may be {@code null}
* @return the {@link Builder}
*/
public Builder country(String country) {
public Builder country(@Nullable String country) {
this.country = country;
return this;
}

View File

@ -20,6 +20,8 @@ import java.net.URL;
import java.time.Instant;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.ClaimAccessor;
/**
@ -43,101 +45,117 @@ import org.springframework.security.oauth2.core.ClaimAccessor;
public interface IdTokenClaimAccessor extends StandardClaimAccessor {
/**
* Returns the Issuer identifier {@code (iss)}.
* @return the Issuer identifier
* Returns the Issuer identifier {@code (iss)}, or {@code null} if it does not exist.
* @return the Issuer identifier, or {@code null} if it does not exist
*/
default URL getIssuer() {
default @Nullable URL getIssuer() {
return this.getClaimAsURL(IdTokenClaimNames.ISS);
}
/**
* Returns the Subject identifier {@code (sub)}.
* @return the Subject identifier
* Returns the Subject identifier {@code (sub)}, or {@code null} if it does not exist.
* @return the Subject identifier, or {@code null} if it does not exist
*/
@Override
default String getSubject() {
default @Nullable String getSubject() {
return this.getClaimAsString(IdTokenClaimNames.SUB);
}
/**
* Returns the Audience(s) {@code (aud)} that this ID Token is intended for.
* @return the Audience(s) that this ID Token is intended for
* Returns the Audience(s) {@code (aud)} that this ID Token is intended for, or
* {@code null} if it does not exist.
* @return the Audience(s) that this ID Token is intended for, or {@code null} if it
* does not exist
*/
default List<String> getAudience() {
default @Nullable List<String> getAudience() {
return this.getClaimAsStringList(IdTokenClaimNames.AUD);
}
/**
* Returns the Expiration time {@code (exp)} on or after which the ID Token MUST NOT
* be accepted.
* @return the Expiration time on or after which the ID Token MUST NOT be accepted
* be accepted, or {@code null} if it does not exist.
* @return the Expiration time on or after which the ID Token MUST NOT be accepted, or
* {@code null} if it does not exist
*/
default Instant getExpiresAt() {
default @Nullable Instant getExpiresAt() {
return this.getClaimAsInstant(IdTokenClaimNames.EXP);
}
/**
* Returns the time at which the ID Token was issued {@code (iat)}.
* @return the time at which the ID Token was issued
* Returns the time at which the ID Token was issued {@code (iat)}, or {@code null} if
* it does not exist.
* @return the time at which the ID Token was issued, or {@code null} if it does not
* exist
*/
default Instant getIssuedAt() {
default @Nullable Instant getIssuedAt() {
return this.getClaimAsInstant(IdTokenClaimNames.IAT);
}
/**
* Returns the time when the End-User authentication occurred {@code (auth_time)}.
* @return the time when the End-User authentication occurred
* Returns the time when the End-User authentication occurred {@code (auth_time)}, or
* {@code null} if it does not exist.
* @return the time when the End-User authentication occurred, or {@code null} if it
* does not exist
*/
default Instant getAuthenticatedAt() {
default @Nullable Instant getAuthenticatedAt() {
return this.getClaimAsInstant(IdTokenClaimNames.AUTH_TIME);
}
/**
* Returns a {@code String} value {@code (nonce)} used to associate a Client session
* with an ID Token, and to mitigate replay attacks.
* @return the nonce used to associate a Client session with an ID Token
* with an ID Token, and to mitigate replay attacks, or {@code null} if it does not
* exist.
* @return the nonce used to associate a Client session with an ID Token, or
* {@code null} if it does not exist
*/
default String getNonce() {
default @Nullable String getNonce() {
return this.getClaimAsString(IdTokenClaimNames.NONCE);
}
/**
* Returns the Authentication Context Class Reference {@code (acr)}.
* @return the Authentication Context Class Reference
* Returns the Authentication Context Class Reference {@code (acr)}, or {@code null}
* if it does not exist.
* @return the Authentication Context Class Reference, or {@code null} if it does not
* exist
*/
default String getAuthenticationContextClass() {
default @Nullable String getAuthenticationContextClass() {
return this.getClaimAsString(IdTokenClaimNames.ACR);
}
/**
* Returns the Authentication Methods References {@code (amr)}.
* @return the Authentication Methods References
* Returns the Authentication Methods References {@code (amr)}, or {@code null} if it
* does not exist.
* @return the Authentication Methods References, or {@code null} if it does not exist
*/
default List<String> getAuthenticationMethods() {
default @Nullable List<String> getAuthenticationMethods() {
return this.getClaimAsStringList(IdTokenClaimNames.AMR);
}
/**
* Returns the Authorized party {@code (azp)} to which the ID Token was issued.
* @return the Authorized party to which the ID Token was issued
* Returns the Authorized party {@code (azp)} to which the ID Token was issued, or
* {@code null} if it does not exist.
* @return the Authorized party to which the ID Token was issued, or {@code null} if
* it does not exist
*/
default String getAuthorizedParty() {
default @Nullable String getAuthorizedParty() {
return this.getClaimAsString(IdTokenClaimNames.AZP);
}
/**
* Returns the Access Token hash value {@code (at_hash)}.
* @return the Access Token hash value
* Returns the Access Token hash value {@code (at_hash)}, or {@code null} if it does
* not exist.
* @return the Access Token hash value, or {@code null} if it does not exist
*/
default String getAccessTokenHash() {
default @Nullable String getAccessTokenHash() {
return this.getClaimAsString(IdTokenClaimNames.AT_HASH);
}
/**
* Returns the Authorization Code hash value {@code (c_hash)}.
* @return the Authorization Code hash value
* Returns the Authorization Code hash value {@code (c_hash)}, or {@code null} if it
* does not exist.
* @return the Authorization Code hash value, or {@code null} if it does not exist
*/
default String getAuthorizationCodeHash() {
default @Nullable String getAuthorizationCodeHash() {
return this.getClaimAsString(IdTokenClaimNames.C_HASH);
}

View File

@ -25,6 +25,8 @@ import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.util.Assert;
@ -57,12 +59,14 @@ public class OidcIdToken extends AbstractOAuth2Token implements IdTokenClaimAcce
/**
* Constructs a {@code OidcIdToken} using the provided parameters.
* @param tokenValue the ID Token value
* @param issuedAt the time at which the ID Token was issued {@code (iat)}
* @param issuedAt the time at which the ID Token was issued {@code (iat)}, may be
* {@code null}
* @param expiresAt the expiration time {@code (exp)} on or after which the ID Token
* MUST NOT be accepted
* MUST NOT be accepted, may be {@code null}
* @param claims the claims about the authentication of the End-User
*/
public OidcIdToken(String tokenValue, Instant issuedAt, Instant expiresAt, Map<String, Object> claims) {
public OidcIdToken(String tokenValue, @Nullable Instant issuedAt, @Nullable Instant expiresAt,
Map<String, Object> claims) {
super(tokenValue, issuedAt, expiresAt);
Assert.notEmpty(claims, "claims cannot be empty");
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
@ -246,12 +250,14 @@ public class OidcIdToken extends AbstractOAuth2Token implements IdTokenClaimAcce
* @return The constructed {@link OidcIdToken}
*/
public OidcIdToken build() {
Instant iat = toInstant(this.claims.get(IdTokenClaimNames.IAT));
Instant exp = toInstant(this.claims.get(IdTokenClaimNames.EXP));
Object iatObj = this.claims.get(IdTokenClaimNames.IAT);
Object expObj = this.claims.get(IdTokenClaimNames.EXP);
Instant iat = toInstant(iatObj);
Instant exp = toInstant(expObj);
return new OidcIdToken(this.tokenValue, iat, exp, this.claims);
}
private Instant toInstant(Object timestamp) {
private @Nullable Instant toInstant(@Nullable Object timestamp) {
if (timestamp != null) {
Assert.isInstanceOf(Instant.class, timestamp, "timestamps must be of type Instant");
}

View File

@ -19,6 +19,8 @@ package org.springframework.security.oauth2.core.oidc;
import java.time.Instant;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.util.CollectionUtils;
@ -41,152 +43,166 @@ import org.springframework.util.CollectionUtils;
public interface StandardClaimAccessor extends ClaimAccessor {
/**
* Returns the Subject identifier {@code (sub)}.
* @return the Subject identifier
* Returns the Subject identifier {@code (sub)}, or {@code null} if it does not exist.
* @return the Subject identifier, or {@code null} if it does not exist
*/
default String getSubject() {
default @Nullable String getSubject() {
return this.getClaimAsString(StandardClaimNames.SUB);
}
/**
* Returns the user's full name {@code (name)} in displayable form.
* @return the user's full name
* Returns the user's full name {@code (name)} in displayable form, or {@code null} if
* it does not exist.
* @return the user's full name, or {@code null} if it does not exist
*/
default String getFullName() {
default @Nullable String getFullName() {
return this.getClaimAsString(StandardClaimNames.NAME);
}
/**
* Returns the user's given name(s) or first name(s) {@code (given_name)}.
* @return the user's given name(s)
* Returns the user's given name(s) or first name(s) {@code (given_name)}, or
* {@code null} if it does not exist.
* @return the user's given name(s), or {@code null} if it does not exist
*/
default String getGivenName() {
default @Nullable String getGivenName() {
return this.getClaimAsString(StandardClaimNames.GIVEN_NAME);
}
/**
* Returns the user's surname(s) or last name(s) {@code (family_name)}.
* @return the user's family names(s)
* Returns the user's surname(s) or last name(s) {@code (family_name)}, or
* {@code null} if it does not exist.
* @return the user's family names(s), or {@code null} if it does not exist
*/
default String getFamilyName() {
default @Nullable String getFamilyName() {
return this.getClaimAsString(StandardClaimNames.FAMILY_NAME);
}
/**
* Returns the user's middle name(s) {@code (middle_name)}.
* @return the user's middle name(s)
* Returns the user's middle name(s) {@code (middle_name)}, or {@code null} if it does
* not exist.
* @return the user's middle name(s), or {@code null} if it does not exist
*/
default String getMiddleName() {
default @Nullable String getMiddleName() {
return this.getClaimAsString(StandardClaimNames.MIDDLE_NAME);
}
/**
* Returns the user's nick name {@code (nickname)} that may or may not be the same as
* the {@code (given_name)}.
* @return the user's nick name
* the {@code (given_name)}, or {@code null} if it does not exist.
* @return the user's nick name, or {@code null} if it does not exist
*/
default String getNickName() {
default @Nullable String getNickName() {
return this.getClaimAsString(StandardClaimNames.NICKNAME);
}
/**
* Returns the preferred username {@code (preferred_username)} that the user wishes to
* be referred to.
* @return the user's preferred user name
* be referred to, or {@code null} if it does not exist.
* @return the user's preferred user name, or {@code null} if it does not exist
*/
default String getPreferredUsername() {
default @Nullable String getPreferredUsername() {
return this.getClaimAsString(StandardClaimNames.PREFERRED_USERNAME);
}
/**
* Returns the URL of the user's profile page {@code (profile)}.
* @return the URL of the user's profile page
* Returns the URL of the user's profile page {@code (profile)}, or {@code null} if it
* does not exist.
* @return the URL of the user's profile page, or {@code null} if it does not exist
*/
default String getProfile() {
default @Nullable String getProfile() {
return this.getClaimAsString(StandardClaimNames.PROFILE);
}
/**
* Returns the URL of the user's profile picture {@code (picture)}.
* @return the URL of the user's profile picture
* Returns the URL of the user's profile picture {@code (picture)}, or {@code null} if
* it does not exist.
* @return the URL of the user's profile picture, or {@code null} if it does not exist
*/
default String getPicture() {
default @Nullable String getPicture() {
return this.getClaimAsString(StandardClaimNames.PICTURE);
}
/**
* Returns the URL of the user's web page or blog {@code (website)}.
* @return the URL of the user's web page or blog
* Returns the URL of the user's web page or blog {@code (website)}, or {@code null}
* if it does not exist.
* @return the URL of the user's web page or blog, or {@code null} if it does not
* exist
*/
default String getWebsite() {
default @Nullable String getWebsite() {
return this.getClaimAsString(StandardClaimNames.WEBSITE);
}
/**
* Returns the user's preferred e-mail address {@code (email)}.
* @return the user's preferred e-mail address
* Returns the user's preferred e-mail address {@code (email)}, or {@code null} if it
* does not exist.
* @return the user's preferred e-mail address, or {@code null} if it does not exist
*/
default String getEmail() {
default @Nullable String getEmail() {
return this.getClaimAsString(StandardClaimNames.EMAIL);
}
/**
* Returns {@code true} if the user's e-mail address has been verified
* {@code (email_verified)}, otherwise {@code false}.
* {@code (email_verified)}, otherwise {@code false}, or {@code null} if it does not
* exist.
* @return {@code true} if the user's e-mail address has been verified, otherwise
* {@code false}
* {@code false}, or {@code null} if it does not exist
*/
default Boolean getEmailVerified() {
default @Nullable Boolean getEmailVerified() {
return this.getClaimAsBoolean(StandardClaimNames.EMAIL_VERIFIED);
}
/**
* Returns the user's gender {@code (gender)}.
* @return the user's gender
* Returns the user's gender {@code (gender)}, or {@code null} if it does not exist.
* @return the user's gender, or {@code null} if it does not exist
*/
default String getGender() {
default @Nullable String getGender() {
return this.getClaimAsString(StandardClaimNames.GENDER);
}
/**
* Returns the user's birth date {@code (birthdate)}.
* @return the user's birth date
* Returns the user's birth date {@code (birthdate)}, or {@code null} if it does not
* exist.
* @return the user's birth date, or {@code null} if it does not exist
*/
default String getBirthdate() {
default @Nullable String getBirthdate() {
return this.getClaimAsString(StandardClaimNames.BIRTHDATE);
}
/**
* Returns the user's time zone {@code (zoneinfo)}.
* @return the user's time zone
* Returns the user's time zone {@code (zoneinfo)}, or {@code null} if it does not
* exist.
* @return the user's time zone, or {@code null} if it does not exist
*/
default String getZoneInfo() {
default @Nullable String getZoneInfo() {
return this.getClaimAsString(StandardClaimNames.ZONEINFO);
}
/**
* Returns the user's locale {@code (locale)}.
* @return the user's locale
* Returns the user's locale {@code (locale)}, or {@code null} if it does not exist.
* @return the user's locale, or {@code null} if it does not exist
*/
default String getLocale() {
default @Nullable String getLocale() {
return this.getClaimAsString(StandardClaimNames.LOCALE);
}
/**
* Returns the user's preferred phone number {@code (phone_number)}.
* @return the user's preferred phone number
* Returns the user's preferred phone number {@code (phone_number)}, or {@code null}
* if it does not exist.
* @return the user's preferred phone number, or {@code null} if it does not exist
*/
default String getPhoneNumber() {
default @Nullable String getPhoneNumber() {
return this.getClaimAsString(StandardClaimNames.PHONE_NUMBER);
}
/**
* Returns {@code true} if the user's phone number has been verified
* {@code (phone_number_verified)}, otherwise {@code false}.
* {@code (phone_number_verified)}, otherwise {@code false}, or {@code null} if it
* does not exist.
* @return {@code true} if the user's phone number has been verified, otherwise
* {@code false}
* {@code false}, or {@code null} if it does not exist
*/
default Boolean getPhoneNumberVerified() {
default @Nullable Boolean getPhoneNumberVerified() {
return this.getClaimAsBoolean(StandardClaimNames.PHONE_NUMBER_VERIFIED);
}
@ -201,10 +217,12 @@ public interface StandardClaimAccessor extends ClaimAccessor {
}
/**
* Returns the time the user's information was last updated {@code (updated_at)}.
* @return the time the user's information was last updated
* Returns the time the user's information was last updated {@code (updated_at)}, or
* {@code null} if it does not exist.
* @return the time the user's information was last updated, or {@code null} if it
* does not exist
*/
default Instant getUpdatedAt() {
default @Nullable Instant getUpdatedAt() {
return this.getClaimAsInstant(StandardClaimNames.UPDATED_AT);
}

View File

@ -18,4 +18,7 @@
* Support classes that model the OpenID Connect Core 1.0 Request and Response messages
* from the Authorization Endpoint and Token Endpoint.
*/
@NullMarked
package org.springframework.security.oauth2.core.oidc.endpoint;
import org.jspecify.annotations.NullMarked;

View File

@ -17,4 +17,7 @@
/**
* Core classes and interfaces providing support for OpenID Connect Core 1.0.
*/
@NullMarked
package org.springframework.security.oauth2.core.oidc;
import org.jspecify.annotations.NullMarked;

View File

@ -20,6 +20,8 @@ import java.io.Serial;
import java.util.Collection;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
@ -48,52 +50,52 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
private final OidcIdToken idToken;
private final OidcUserInfo userInfo;
private final @Nullable OidcUserInfo userInfo;
/**
* Constructs a {@code DefaultOidcUser} using the provided parameters.
* @param authorities the authorities granted to the user
* @param authorities the authorities granted to the user, may be {@code null}
* @param idToken the {@link OidcIdToken ID Token} containing claims about the user
*/
public DefaultOidcUser(Collection<? extends GrantedAuthority> authorities, OidcIdToken idToken) {
public DefaultOidcUser(@Nullable Collection<? extends GrantedAuthority> authorities, OidcIdToken idToken) {
this(authorities, idToken, IdTokenClaimNames.SUB);
}
/**
* Constructs a {@code DefaultOidcUser} using the provided parameters.
* @param authorities the authorities granted to the user
* @param authorities the authorities granted to the user, may be {@code null}
* @param idToken the {@link OidcIdToken ID Token} containing claims about the user
* @param nameAttributeKey the key used to access the user's &quot;name&quot; from
* {@link #getAttributes()}
*/
public DefaultOidcUser(Collection<? extends GrantedAuthority> authorities, OidcIdToken idToken,
public DefaultOidcUser(@Nullable Collection<? extends GrantedAuthority> authorities, OidcIdToken idToken,
String nameAttributeKey) {
this(authorities, idToken, null, nameAttributeKey);
}
/**
* Constructs a {@code DefaultOidcUser} using the provided parameters.
* @param authorities the authorities granted to the user
* @param authorities the authorities granted to the user, may be {@code null}
* @param idToken the {@link OidcIdToken ID Token} containing claims about the user
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
* may be {@code null}
*/
public DefaultOidcUser(Collection<? extends GrantedAuthority> authorities, OidcIdToken idToken,
OidcUserInfo userInfo) {
public DefaultOidcUser(@Nullable Collection<? extends GrantedAuthority> authorities, OidcIdToken idToken,
@Nullable OidcUserInfo userInfo) {
this(authorities, idToken, userInfo, IdTokenClaimNames.SUB);
}
/**
* Constructs a {@code DefaultOidcUser} using the provided parameters.
* @param authorities the authorities granted to the user
* @param authorities the authorities granted to the user, may be {@code null}
* @param idToken the {@link OidcIdToken ID Token} containing claims about the user
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
* may be {@code null}
* @param nameAttributeKey the key used to access the user's &quot;name&quot; from
* {@link #getAttributes()}
*/
public DefaultOidcUser(Collection<? extends GrantedAuthority> authorities, OidcIdToken idToken,
OidcUserInfo userInfo, String nameAttributeKey) {
public DefaultOidcUser(@Nullable Collection<? extends GrantedAuthority> authorities, OidcIdToken idToken,
@Nullable OidcUserInfo userInfo, String nameAttributeKey) {
super(authorities, OidcUserAuthority.collectClaims(idToken, userInfo), nameAttributeKey);
this.idToken = idToken;
this.userInfo = userInfo;
@ -110,7 +112,7 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
}
@Override
public OidcUserInfo getUserInfo() {
public @Nullable OidcUserInfo getUserInfo() {
return this.userInfo;
}

View File

@ -18,6 +18,8 @@ package org.springframework.security.oauth2.core.oidc.user;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimAccessor;
@ -65,10 +67,11 @@ public interface OidcUser extends OAuth2User, IdTokenClaimAccessor {
Map<String, Object> getClaims();
/**
* Returns the {@link OidcUserInfo UserInfo} containing claims about the user.
* @return the {@link OidcUserInfo} containing claims about the user.
* Returns the {@link OidcUserInfo UserInfo} containing claims about the user, or
* {@code null} if not present.
* @return the {@link OidcUserInfo} containing claims about the user, or {@code null}
*/
OidcUserInfo getUserInfo();
@Nullable OidcUserInfo getUserInfo();
/**
* Returns the {@link OidcIdToken ID Token} containing claims about the user.

View File

@ -19,8 +19,10 @@ package org.springframework.security.oauth2.core.oidc.user;
import java.io.Serial;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
import org.springframework.lang.Nullable;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
@ -42,7 +44,7 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
private final OidcIdToken idToken;
private final OidcUserInfo userInfo;
private final @Nullable OidcUserInfo userInfo;
/**
* Constructs a {@code OidcUserAuthority} using the provided parameters.
@ -59,7 +61,7 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
* may be {@code null}
*/
public OidcUserAuthority(OidcIdToken idToken, OidcUserInfo userInfo) {
public OidcUserAuthority(OidcIdToken idToken, @Nullable OidcUserInfo userInfo) {
this("OIDC_USER", idToken, userInfo);
}
@ -70,10 +72,11 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
* may be {@code null}
* @param userNameAttributeName the attribute name used to access the user's name from
* the attributes
* the attributes, may be {@code null}
* @since 6.4
*/
public OidcUserAuthority(OidcIdToken idToken, OidcUserInfo userInfo, @Nullable String userNameAttributeName) {
public OidcUserAuthority(OidcIdToken idToken, @Nullable OidcUserInfo userInfo,
@Nullable String userNameAttributeName) {
this("OIDC_USER", idToken, userInfo, userNameAttributeName);
}
@ -84,7 +87,7 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
* may be {@code null}
*/
public OidcUserAuthority(String authority, OidcIdToken idToken, OidcUserInfo userInfo) {
public OidcUserAuthority(String authority, OidcIdToken idToken, @Nullable OidcUserInfo userInfo) {
this(authority, idToken, userInfo, IdTokenClaimNames.SUB);
}
@ -95,10 +98,10 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
* may be {@code null}
* @param userNameAttributeName the attribute name used to access the user's name from
* the attributes
* the attributes, may be {@code null}
* @since 6.4
*/
public OidcUserAuthority(String authority, OidcIdToken idToken, OidcUserInfo userInfo,
public OidcUserAuthority(String authority, OidcIdToken idToken, @Nullable OidcUserInfo userInfo,
@Nullable String userNameAttributeName) {
super(authority, collectClaims(idToken, userInfo), userNameAttributeName);
this.idToken = idToken;
@ -118,7 +121,7 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
* {@code null}.
* @return the {@link OidcUserInfo} containing claims about the user, or {@code null}
*/
public OidcUserInfo getUserInfo() {
public @Nullable OidcUserInfo getUserInfo() {
return this.userInfo;
}
@ -137,8 +140,7 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
if (!this.getIdToken().equals(that.getIdToken())) {
return false;
}
return (this.getUserInfo() != null) ? this.getUserInfo().equals(that.getUserInfo())
: that.getUserInfo() == null;
return Objects.equals(this.getUserInfo(), that.getUserInfo());
}
@Override
@ -149,7 +151,7 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
return result;
}
static Map<String, Object> collectClaims(OidcIdToken idToken, OidcUserInfo userInfo) {
static Map<String, Object> collectClaims(OidcIdToken idToken, @Nullable OidcUserInfo userInfo) {
Assert.notNull(idToken, "idToken cannot be null");
Map<String, Object> claims = new HashMap<>();
if (userInfo != null) {

View File

@ -18,4 +18,7 @@
* Provides a model for an OpenID Connect Core 1.0 representation of a user
* {@code Principal}.
*/
@NullMarked
package org.springframework.security.oauth2.core.oidc.user;
import org.jspecify.annotations.NullMarked;

View File

@ -18,4 +18,7 @@
* Core classes and interfaces providing support for the OAuth 2.0 Authorization
* Framework.
*/
@NullMarked
package org.springframework.security.oauth2.core;
import org.jspecify.annotations.NullMarked;

View File

@ -27,6 +27,8 @@ import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.util.Assert;
@ -59,17 +61,17 @@ public class DefaultOAuth2User implements OAuth2User, Serializable {
/**
* Constructs a {@code DefaultOAuth2User} using the provided parameters.
* @param authorities the authorities granted to the user
* @param authorities the authorities granted to the user, may be {@code null}
* @param attributes the attributes about the user
* @param nameAttributeKey the key used to access the user's &quot;name&quot; from
* {@link #getAttributes()}
*/
public DefaultOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes,
String nameAttributeKey) {
public DefaultOAuth2User(@Nullable Collection<? extends GrantedAuthority> authorities,
Map<String, Object> attributes, String nameAttributeKey) {
Assert.notEmpty(attributes, "attributes cannot be empty");
Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty");
Assert.notNull(attributes.get(nameAttributeKey),
"Attribute value for '" + nameAttributeKey + "' cannot be null");
Object nameAttributeValue = attributes.get(nameAttributeKey);
Assert.notNull(nameAttributeValue, "Attribute value for '" + nameAttributeKey + "' cannot be null");
this.authorities = (authorities != null)
? Collections.unmodifiableSet(new LinkedHashSet<>(this.sortAuthorities(authorities)))
@ -80,7 +82,9 @@ public class DefaultOAuth2User implements OAuth2User, Serializable {
@Override
public String getName() {
return this.getAttribute(this.nameAttributeKey).toString();
Object nameAttributeValue = this.getAttribute(this.nameAttributeKey);
Assert.notNull(nameAttributeValue, "Name attribute value cannot be null");
return nameAttributeValue.toString();
}
@Override

View File

@ -22,7 +22,8 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
@ -41,7 +42,7 @@ public class OAuth2UserAuthority implements GrantedAuthority {
private final Map<String, Object> attributes;
private final String userNameAttributeName;
private final @Nullable String userNameAttributeName;
/**
* Constructs a {@code OAuth2UserAuthority} using the provided parameters and defaults
@ -78,10 +79,11 @@ public class OAuth2UserAuthority implements GrantedAuthority {
* @param authority the authority granted to the user
* @param attributes the attributes about the user
* @param userNameAttributeName the attribute name used to access the user's name from
* the attributes
* the attributes, may be {@code null}
* @since 6.4
*/
public OAuth2UserAuthority(String authority, Map<String, Object> attributes, String userNameAttributeName) {
public OAuth2UserAuthority(String authority, Map<String, Object> attributes,
@Nullable String userNameAttributeName) {
Assert.hasText(authority, "authority cannot be empty");
Assert.notEmpty(attributes, "attributes cannot be empty");
this.authority = authority;
@ -104,11 +106,11 @@ public class OAuth2UserAuthority implements GrantedAuthority {
/**
* Returns the attribute name used to access the user's name from the attributes.
* @return the attribute name used to access the user's name from the attributes
* @return the attribute name used to access the user's name from the attributes, or
* {@code null} if not available
* @since 6.4
*/
@Nullable
public String getUserNameAttributeName() {
public @Nullable String getUserNameAttributeName() {
return this.userNameAttributeName;
}
@ -137,8 +139,9 @@ public class OAuth2UserAuthority implements GrantedAuthority {
}
}
else {
Object thatValue = convertURLIfNecessary(thatAttributes.get(key));
if (!value.equals(thatValue)) {
Object thatValue = thatAttributes.get(key);
Object convertedThatValue = convertURLIfNecessary(thatValue);
if (!value.equals(convertedThatValue)) {
return false;
}
}
@ -165,9 +168,10 @@ public class OAuth2UserAuthority implements GrantedAuthority {
/**
* @return {@code URL} converted to a string since {@code URL} shouldn't be used for
* equality/hashCode. For other instances the value is returned as is.
* equality/hashCode. For other instances the value is returned as is (including
* null).
*/
private static Object convertURLIfNecessary(Object value) {
private static @Nullable Object convertURLIfNecessary(@Nullable Object value) {
return (value instanceof URL) ? ((URL) value).toExternalForm() : value;
}

View File

@ -17,4 +17,7 @@
/**
* Provides a model for an OAuth 2.0 representation of a user {@code Principal}.
*/
@NullMarked
package org.springframework.security.oauth2.core.user;
import org.jspecify.annotations.NullMarked;

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* Web support classes for OAuth 2.0 and OpenID Connect.
*/
@NullMarked
package org.springframework.security.oauth2.core.web;
import org.jspecify.annotations.NullMarked;

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* Reactive functional web support classes for OAuth 2.0 and OpenID Connect.
*/
@NullMarked
package org.springframework.security.oauth2.core.web.reactive.function;
import org.jspecify.annotations.NullMarked;

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* Reactive web support classes for OAuth 2.0 and OpenID Connect.
*/
@NullMarked
package org.springframework.security.oauth2.core.web.reactive;
import org.jspecify.annotations.NullMarked;

View File

@ -76,11 +76,11 @@ public class BearerTokenAuthenticationTests {
}
@Test
public void getNameWhenHasNoSubjectThenReturnsNull() {
public void getNameWhenHasNoSubjectThenReturnsEmptyString() {
OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal(
Collections.singletonMap("claim", "value"), null);
BearerTokenAuthentication authenticated = new BearerTokenAuthentication(principal, this.token, null);
assertThat(authenticated.getName()).isNull();
assertThat(authenticated.getName()).isEmpty();
}
@Test