-
Notifications
You must be signed in to change notification settings - Fork 6.1k
API key authentication implementation #17800
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
+1,175
−0
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
148 changes: 148 additions & 0 deletions
...java/org/springframework/security/config/annotation/web/configurers/ApiKeyConfigurer.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
package org.springframework.security.config.annotation.web.configurers; | ||
|
||
import java.time.Clock; | ||
import java.util.Collection; | ||
import java.util.Objects; | ||
|
||
import org.springframework.context.ApplicationContext; | ||
import org.springframework.core.convert.converter.Converter; | ||
import org.springframework.security.authentication.AuthenticationManager; | ||
import org.springframework.security.authentication.apikey.*; | ||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder; | ||
import org.springframework.security.core.GrantedAuthority; | ||
import org.springframework.security.core.context.SecurityContextHolderStrategy; | ||
import org.springframework.security.web.authentication.AuthenticationConverter; | ||
import org.springframework.security.web.authentication.AuthenticationFailureHandler; | ||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; | ||
import org.springframework.security.web.authentication.apikey.ApiKeyAuthenticationFilter; | ||
import org.springframework.security.web.authentication.apikey.BearerTokenAuthenticationConverter; | ||
import org.springframework.security.web.context.NullSecurityContextRepository; | ||
import org.springframework.security.web.context.SecurityContextRepository; | ||
|
||
/** | ||
* Configures API key authentication. | ||
* | ||
* @author Alexey Razinkov | ||
*/ | ||
public final class ApiKeyConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<ApiKeyConfigurer<H>, H> { | ||
|
||
private final ApplicationContext context; | ||
|
||
private Clock clock = Clock.systemUTC(); | ||
|
||
private Converter<StoredApiKey, Collection<GrantedAuthority>> grantedAuthorityConverter = | ||
new ApiKeySimpleGrantedAuthorityConverter(); | ||
|
||
private ApiKeySearchService searchService; | ||
|
||
private ApiKeyDigest digest; | ||
|
||
private AuthenticationConverter authnConverter = new BearerTokenAuthenticationConverter(); | ||
|
||
private SecurityContextRepository securityContextRepository = new NullSecurityContextRepository(); | ||
|
||
private AuthenticationSuccessHandler successHandler; | ||
|
||
private AuthenticationFailureHandler failureHandler; | ||
|
||
public ApiKeyConfigurer(final ApplicationContext context) { | ||
this.context = Objects.requireNonNull(context); | ||
} | ||
|
||
public ApiKeyConfigurer<H> clock(final Clock clock) { | ||
this.clock = Objects.requireNonNull(clock); | ||
return this; | ||
} | ||
|
||
public ApiKeyConfigurer<H> grantedAuthorityConverter( | ||
final Converter<StoredApiKey, Collection<GrantedAuthority>> converter | ||
) { | ||
this.grantedAuthorityConverter = Objects.requireNonNull(converter); | ||
return this; | ||
} | ||
|
||
public ApiKeyConfigurer<H> searchService(final ApiKeySearchService searchService) { | ||
this.searchService = Objects.requireNonNull(searchService); | ||
return this; | ||
} | ||
|
||
public ApiKeyConfigurer<H> digest(final ApiKeyDigest digest) { | ||
this.digest = Objects.requireNonNull(digest); | ||
return this; | ||
} | ||
|
||
public ApiKeyConfigurer<H> authenticationConverter(final AuthenticationConverter converter) { | ||
this.authnConverter = Objects.requireNonNull(converter); | ||
return this; | ||
} | ||
|
||
public ApiKeyConfigurer<H> securityContextRepository(final SecurityContextRepository securityContextRepository) { | ||
this.securityContextRepository = Objects.requireNonNull(securityContextRepository); | ||
return this; | ||
} | ||
|
||
public ApiKeyConfigurer<H> authenticationSuccessHandler(final AuthenticationSuccessHandler successHandler) { | ||
this.successHandler = successHandler; | ||
return this; | ||
} | ||
|
||
public ApiKeyConfigurer<H> authenticationFailureHandler(final AuthenticationFailureHandler failureHandler) { | ||
this.failureHandler = failureHandler; | ||
return this; | ||
} | ||
|
||
@Override | ||
public void init(final H builder) throws Exception { | ||
super.init(builder); | ||
final ApiKeySearchService searchService = getSearchService(); | ||
final ApiKeyDigest digest = getDigest(); | ||
final ApiKeyAuthenticationProvider authnProvider = new ApiKeyAuthenticationProvider( | ||
searchService, | ||
digest, | ||
this.clock, | ||
this.grantedAuthorityConverter | ||
); | ||
builder.authenticationProvider(authnProvider); | ||
} | ||
|
||
private ApiKeySearchService getSearchService() { | ||
if (this.searchService != null) { | ||
return this.searchService; | ||
} | ||
|
||
final ApiKeySearchService bean = this.context.getBean(ApiKeySearchService.class); | ||
if (bean == null) { | ||
throw new IllegalStateException("API key search service required"); | ||
} | ||
|
||
return bean; | ||
} | ||
|
||
private ApiKeyDigest getDigest() { | ||
if (this.digest != null) { | ||
return this.digest; | ||
} | ||
|
||
final ApiKeyDigest bean = this.context.getBean(ApiKeyDigest.class); | ||
if (bean == null) { | ||
throw new IllegalStateException("API key digest required"); | ||
} | ||
|
||
return bean; | ||
} | ||
|
||
@Override | ||
public void configure(final H http) { | ||
final AuthenticationManager authnManager = http.getSharedObject(AuthenticationManager.class); | ||
final SecurityContextHolderStrategy securityContextHolderStrategy = getSecurityContextHolderStrategy(); | ||
final ApiKeyAuthenticationFilter filter = new ApiKeyAuthenticationFilter( | ||
authnManager, | ||
this.authnConverter, | ||
securityContextHolderStrategy, | ||
this.securityContextRepository, | ||
this.successHandler, | ||
this.failureHandler | ||
); | ||
http.addFilter(postProcess(filter)); | ||
} | ||
} |
132 changes: 132 additions & 0 deletions
core/src/main/java/org/springframework/security/authentication/apikey/ApiKey.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
package org.springframework.security.authentication.apikey; | ||
|
||
import java.io.Serial; | ||
import java.io.Serializable; | ||
import java.security.SecureRandom; | ||
import java.util.Arrays; | ||
import java.util.Base64; | ||
import java.util.Objects; | ||
import java.util.function.Function; | ||
import java.util.random.RandomGenerator; | ||
|
||
import org.springframework.util.Assert; | ||
|
||
/** | ||
* API key that consists ID and secret parts. | ||
* <p> | ||
* ID part allows efficiently searching API key information in some storage, such as | ||
* relational database ({@link ApiKeySearchService} interface is used for this purpose). | ||
* API key ID should not be used alone as it's prune to timing attacks (storages cannot | ||
* use constant-time comparison because of efficiency requirements), so separate secret | ||
* part is used. | ||
* <p> | ||
* Secret part should be hashed before storing API key data just the same way as | ||
* passwords, except that API keys do not require using specific slow hashing algorithms | ||
* used for passwords (such BCrypt, Argon, etc.). | ||
* | ||
* @author Alexey Razinkov | ||
*/ | ||
public final class ApiKey implements Serializable { | ||
|
||
@Serial | ||
private static final long serialVersionUID = 5948279771096057355L; | ||
|
||
public static final SecureRandom RND = new SecureRandom(); | ||
|
||
public static final int DEFAULT_ID_BYTES_LENGTH = 16; | ||
|
||
public static final int DEFAULT_SECRET_BYTES_LENGTH = 16; | ||
|
||
public static ApiKey random() { | ||
return random(RND, DEFAULT_ID_BYTES_LENGTH, DEFAULT_SECRET_BYTES_LENGTH); | ||
} | ||
|
||
public static ApiKey random(final RandomGenerator random, final int idBytesLength, final int secretBytesLength) { | ||
Objects.requireNonNull(random); | ||
final byte[] idBytes = new byte[idBytesLength]; | ||
final byte[] secretBytes = new byte[secretBytesLength]; | ||
random.nextBytes(idBytes); | ||
random.nextBytes(secretBytes); | ||
return new ApiKey(idBytes, secretBytes); | ||
} | ||
|
||
public static ApiKey parse(final String value) { | ||
return parse(value, DEFAULT_ENCODER, DEFAULT_DECODER); | ||
} | ||
|
||
public static ApiKey parse(final String value, final Function<byte[], String> encoder, | ||
final Function<String, byte[]> decoder) { | ||
Assert.hasText(value, "API key must be provided"); | ||
Objects.requireNonNull(encoder); | ||
Objects.requireNonNull(decoder); | ||
|
||
final String[] parts = value.split("_", -1); | ||
Assert.isTrue(parts.length == 2, "API key has invalid format"); | ||
|
||
final String apiKeyId = parts[0]; | ||
Assert.hasText(apiKeyId, "API key has invalid format"); | ||
|
||
final String apiKeySecret = parts[1]; | ||
Assert.hasText(apiKeySecret, "API key has invalid format"); | ||
|
||
return new ApiKey(apiKeyId, decoder.apply(apiKeySecret), encoder); | ||
} | ||
|
||
private final String id; | ||
|
||
private final byte[] secret; | ||
|
||
private final Function<byte[], String> encoder; | ||
|
||
private ApiKey(final byte[] id, final byte[] secret) { | ||
this(DEFAULT_ENCODER.apply(id), secret, DEFAULT_ENCODER); | ||
} | ||
|
||
private ApiKey(final String id, final byte[] secret, final Function<byte[], String> encoder) { | ||
Assert.hasText(id, "API key ID cannot be empty"); | ||
Assert.isTrue(secret != null && secret.length > 0, "API key secret required"); | ||
Objects.requireNonNull(encoder); | ||
this.id = id; | ||
this.secret = Arrays.copyOf(secret, secret.length); | ||
this.encoder = encoder; | ||
} | ||
|
||
public String getId() { | ||
return this.id; | ||
} | ||
|
||
public byte[] getSecret() { | ||
return Arrays.copyOf(this.secret, this.secret.length); | ||
} | ||
|
||
public String asToken() { | ||
return this.id + '_' + this.encoder.apply(this.secret); | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return "DefaultApiKey{id='" + this.id + '}'; | ||
} | ||
|
||
private static final Function<byte[], String> DEFAULT_ENCODER = Base64.getEncoder() | ||
.withoutPadding()::encodeToString; | ||
|
||
private static final Function<String, byte[]> DEFAULT_DECODER = Base64.getDecoder()::decode; | ||
|
||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.