Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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
noavarice wants to merge 1 commit into spring-projects:main
base: main
Choose a base branch
Loading
from noavarice:api-keys
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer;
import org.springframework.security.config.annotation.web.configurers.ApiKeyConfigurer;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry;
import org.springframework.security.config.annotation.web.configurers.ChannelSecurityConfigurer;
Expand Down Expand Up @@ -1591,6 +1592,71 @@ public HttpSecurity oneTimeTokenLogin(
return HttpSecurity.this;
}

/**
* Configures API key authentication support.
*
* <h2>Example Configuration</h2>
*
* <pre>
* &#064;Configuration
* &#064;EnableWebSecurity
* public class SecurityConfig {
*
* &#064;Bean
* public ApiKeyDigest apiKeyDigest() {
* return new Sha3ApiKeyDigest();
* }
*
* &#064;Bean
* public ApiKeySearchService apiKeySearchService(JdbcTemplate jdbc) {
* return new ApiKeySearchServiceImpl(jdbc);
* }
*
* // separate filter chain for service-to-service requests
* &#064;Bean
* &#064;Order(1)
* public SecurityFilterChain apiKeySecurityFilterChain(
* HttpSecurity http,
* ApiKeyDigest digest,
* ApiKeySearchService searchService
* ) throws Exception {
* return http
* .securityMatcher("/s2s/do-something")
* .authorizeHttpRequests((authorize) -&gt; authorize
* .anyRequest().authenticated()
* )
* .apiKey(configurer -> configurer
* .digest(digest)
* .searchService(searchService())
* )
* // API key authentication is used for server-to-service interactions
* // which means there SHOULD be no browser, so no possibility for CSRF
* .csrf(AbstractHttpConfigurer::disable)
* .build();
* }
*
* // filter chain for user requests
* &#064;Bean
* &#064;Order(2)
* public SecurityFilterChain securityFilterChain(
* HttpSecurity http,
* ApiKeyDigest digest,
* ApiKeySearchService searchService
* ) throws Exception {
* // configure as usual
* }
*
* }
* </pre>
* @param configurerCustomizer the {@link Customizer} to provide more options for the {@link ApiKeyConfigurer}
* @return the {@link HttpSecurity} for further customizations
* @throws Exception
*/
public HttpSecurity apiKey( Customizer<ApiKeyConfigurer<HttpSecurity>> configurerCustomizer) throws Exception {
configurerCustomizer.customize(getOrApply(new ApiKeyConfigurer<>(getContext())));
return HttpSecurity.this;
}

/**
* Configures channel security. In order for this configuration to be useful at least
* one mapping to a required channel must be provided.
Expand Down
View file Open in desktop
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));
}
}
View file Open in desktop
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;

}
Loading

AltStyle によって変換されたページ (->オリジナル) /