Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29f8dd53cd | |||
| 7526dfa694 | |||
| 02f5f5a459 | |||
| 3dff446d18 | |||
| cc89707b03 | |||
| 812ad9c547 | |||
| 364c153e17 | |||
| 39383ab970 | |||
| 8ef5bcd003 | |||
| 4193b73d47 | |||
| 4ce48297f6 | |||
| d09463b865 | |||
| 9989735a49 | |||
| c7be397a16 | |||
| 02cc2e9d14 | |||
| 118e51d45e | |||
| 27a76a67ae | |||
| ff6cef0cd2 | |||
| 2e52e1063a | |||
| 801f890ed0 | |||
| 1a7769be28 | |||
| bdf501fe87 | |||
| b26d968c1c | |||
| 3070c96594 | |||
| 6eb7ef6e6d | |||
| 988ba6b33c | |||
| d1fb6141c1 | |||
| 43f5ae5b21 | |||
| b2a6bbf314 | |||
| a161421409 | |||
| 36aa2ad744 | |||
| ed4e0e2ea8 | |||
| 978eb4396a | |||
| 770c860d9d | |||
| 11890f2f49 | |||
| 5809451de7 | |||
| 31d379a7bf | |||
| 825370f8e3 | |||
| 61feae94a0 | |||
| edaae1d17d | |||
| e8084090ff | |||
| 4773b40f02 | |||
| 9a4a965b0d | |||
| 0bae8a73d1 | |||
| 461ccd817d | |||
| d449950b36 | |||
| 8dc341af8c | |||
| 381c556f78 | |||
| 19afc9121e | |||
| 6b2f892e66 | |||
| 3b276e6c0d | |||
| f2115fbef8 | |||
| 298335a57e | |||
| c6566ee772 | |||
| 7442214225 | |||
| 0123ae0981 | |||
| 11a648d106 | |||
| 536e09756c | |||
| ac15cdbea3 | |||
| ad812aefc4 | |||
| d8769fb183 | |||
| ae997a4f46 | |||
| bbc6273abf | |||
| 475af0d3db | |||
| f8a61e66d1 | |||
| 8830e39073 | |||
| 304cc21523 | |||
| 7048b08b9c | |||
| ff108a49a5 | |||
| 9a70d73036 | |||
| cf613ad2d3 | |||
| fdec2d7617 | |||
| 748e3c9cf0 | |||
| 057581584a | |||
| 502ecf9241 | |||
| 7db67f923c | |||
| 35859220cb | |||
| 3836091fe2 | |||
| 4c41928bc7 | |||
| 61be628ad6 | |||
| e6208a4832 | |||
| 01452e328f | |||
| 836190546d | |||
| 9f94fb92c5 | |||
| 47804ee834 | |||
| 865085e0b0 | |||
| 15a31d7dd7 | |||
| 1fe59232f0 | |||
| 8f13ab216f | |||
| 8beb08974b | |||
| 49c89546c6 | |||
| 5a74026036 | |||
| c9220ca39e | |||
| b6b9d743a0 | |||
| 8ded9dac6a | |||
| 1b84c74776 | |||
| 83dfc0790e | |||
| b55f66695e | |||
| 177e482783 | |||
| 04d95818e3 | |||
| 627a412748 | |||
| addebb61ed | |||
| 75d985fb13 | |||
| 342b24d43e | |||
| 2f97d8dac2 | |||
| 9d8026fcf3 | |||
| 3881de5d6c | |||
| 1fa37a9479 | |||
| 4e92ea215c | |||
| d111029078 | |||
| d7ea83017a | |||
| 6401fde9f2 | |||
| 7491e37310 | |||
| d90714f07d | |||
| 3cf0867955 | |||
| 7f219ea530 | |||
| b622028f84 | |||
| 8b92e71cb2 | |||
| 2c0c09b9a6 | |||
| 6ed154527e |
@@ -4,7 +4,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- '*.x'
|
|
||||||
- 'docs-build'
|
- 'docs-build'
|
||||||
|
|
||||||
run-name: Merge Dependabot PR ${{ github.ref_name }}
|
run-name: Merge Dependabot PR ${{ github.ref_name }}
|
||||||
@@ -14,4 +13,4 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@0d3f15bb384839966a1ff5c4383731a2b747f24b # v7
|
uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@0d3f15bb384839966a1ff5c4383731a2b747f24b # v7
|
||||||
with:
|
with:
|
||||||
mergeArguments: --auto --rebase
|
mergeArguments: --auto --rebase
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest, windows-latest ]
|
os: [ ubuntu-latest, windows-latest ]
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
deploy-schema:
|
deploy-schema:
|
||||||
name: Deploy Schema
|
name: Deploy Schema
|
||||||
needs: [ build ]
|
needs: [ build ]
|
||||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
|
||||||
with:
|
with:
|
||||||
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
|
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||||
java-version: 25
|
java-version: 25
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ jobs:
|
|||||||
uses: spring-io/spring-release-actions/compute-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
|
uses: spring-io/spring-release-actions/compute-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
|
||||||
- name: Get Today's Release Version
|
- name: Get Today's Release Version
|
||||||
id: todays-release
|
id: todays-release
|
||||||
uses: spring-io/spring-release-actions/get-todays-release-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
|
uses: spring-io/spring-release-actions/get-todays-release-version@1b8671612c3eb3d9b9763e2d7b66f1a80d00ee95 # 0.0.6
|
||||||
with:
|
with:
|
||||||
snapshot-version: ${{ steps.compute-version.outputs.version }}
|
snapshot-version: ${{ steps.compute-version.outputs.version }}
|
||||||
milestone-repository: ${{ github.repository }}
|
milestone-repository: ${{ github.repository }}
|
||||||
milestone-token: ${{ secrets.GITHUB_TOKEN }}
|
milestone-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Compute Next Version
|
- name: Compute Next Version
|
||||||
id: next-version
|
id: next-version
|
||||||
uses: spring-io/spring-release-actions/compute-next-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
|
uses: spring-io/spring-release-actions/compute-next-version@1b8671612c3eb3d9b9763e2d7b66f1a80d00ee95 # 0.0.6
|
||||||
with:
|
with:
|
||||||
version: ${{ steps.todays-release.outputs.release-version }}
|
version: ${{ steps.todays-release.outputs.release-version }}
|
||||||
- name: Schedule Next Milestone
|
- name: Schedule Next Milestone
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- name: Set up JDK 25
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
java-version: '25'
|
java-version: '25'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: Release Announcements - Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: The version to publish
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-schema:
|
||||||
|
name: Deploy Schema
|
||||||
|
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
|
||||||
|
with:
|
||||||
|
should-deploy-schema: true
|
||||||
|
secrets: inherit
|
||||||
|
publish-release-announcements:
|
||||||
|
name: Publish Release Announcements
|
||||||
|
needs: [ deploy-schema ]
|
||||||
|
if: ${{ !github.event.repository.fork }}
|
||||||
|
uses: spring-io/spring-security-release-tools/.github/workflows/release-announcements-publish.yml@d6c65d3013c0888e2c9cbae9f4beda610994776c # v1.0.16
|
||||||
|
with:
|
||||||
|
version: ${{ inputs.version }}
|
||||||
|
secrets: inherit
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: Release Announcements - Stage
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: The version to stage
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stage-release-announcements:
|
||||||
|
name: Stage Release Announcements
|
||||||
|
if: ${{ !github.event.repository.fork }}
|
||||||
|
uses: spring-io/spring-security-release-tools/.github/workflows/release-announcements-stage.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
|
||||||
|
with:
|
||||||
|
version: ${{ inputs.version || github.ref_name }}
|
||||||
|
secrets: inherit
|
||||||
+2
-1
@@ -17,6 +17,7 @@
|
|||||||
package org.springframework.security.config.annotation.authentication.configurers.ldap;
|
package org.springframework.security.config.annotation.authentication.configurers.ldap;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
|
|
||||||
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
|
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
|
||||||
@@ -590,7 +591,7 @@ public class LdapAuthenticationProviderConfigurer<B extends ProviderManagerBuild
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int getDefaultPort() {
|
private int getDefaultPort() {
|
||||||
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) {
|
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT, 50, InetAddress.getLoopbackAddress())) {
|
||||||
return serverSocket.getLocalPort();
|
return serverSocket.getLocalPort();
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
|
|||||||
+6
@@ -32,6 +32,7 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.context.annotation.ImportAware;
|
import org.springframework.context.annotation.ImportAware;
|
||||||
import org.springframework.context.annotation.Role;
|
import org.springframework.context.annotation.Role;
|
||||||
import org.springframework.core.type.AnnotationMetadata;
|
import org.springframework.core.type.AnnotationMetadata;
|
||||||
|
import org.springframework.security.access.PermissionEvaluator;
|
||||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||||
@@ -127,6 +128,11 @@ final class PrePostMethodSecurityConfiguration implements ImportAware, Applicati
|
|||||||
this.expressionHandler.setAuthorizationManagerFactory(authorizationManagerFactory);
|
this.expressionHandler.setAuthorizationManagerFactory(authorizationManagerFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
|
||||||
|
this.expressionHandler.setPermissionEvaluator(permissionEvaluator);
|
||||||
|
}
|
||||||
|
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) {
|
void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) {
|
||||||
this.preFilterMethodInterceptor.setTemplateDefaults(templateDefaults);
|
this.preFilterMethodInterceptor.setTemplateDefaults(templateDefaults);
|
||||||
|
|||||||
+3
-1
@@ -706,8 +706,10 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void configure(H http) {
|
private void configure(H http) {
|
||||||
|
DPoPAuthenticationProvider authenticationProvider = new DPoPAuthenticationProvider(
|
||||||
|
getTokenAuthenticationManager(http));
|
||||||
|
http.authenticationProvider(postProcess(authenticationProvider));
|
||||||
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
|
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
|
||||||
http.authenticationProvider(new DPoPAuthenticationProvider(getTokenAuthenticationManager(http)));
|
|
||||||
AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager,
|
AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager,
|
||||||
getAuthenticationConverter());
|
getAuthenticationConverter());
|
||||||
authenticationFilter.setRequestMatcher(getRequestMatcher());
|
authenticationFilter.setRequestMatcher(getRequestMatcher());
|
||||||
|
|||||||
+2
-1
@@ -17,6 +17,7 @@
|
|||||||
package org.springframework.security.config.ldap;
|
package org.springframework.security.config.ldap;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
|
|
||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
@@ -188,7 +189,7 @@ public class LdapServerBeanDefinitionParser implements BeanDefinitionParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getDefaultPort() {
|
private String getDefaultPort() {
|
||||||
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) {
|
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT, 50, InetAddress.getLoopbackAddress())) {
|
||||||
return String.valueOf(serverSocket.getLocalPort());
|
return String.valueOf(serverSocket.getLocalPort());
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
|
|||||||
+27
@@ -297,6 +297,7 @@ import org.springframework.web.util.pattern.PathPatternParser;
|
|||||||
* @author Ankur Pathak
|
* @author Ankur Pathak
|
||||||
* @author Alexey Nesterov
|
* @author Alexey Nesterov
|
||||||
* @author Yanming Zhou
|
* @author Yanming Zhou
|
||||||
|
* @author Iain Henderson
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
*/
|
*/
|
||||||
public class ServerHttpSecurity {
|
public class ServerHttpSecurity {
|
||||||
@@ -4138,6 +4139,8 @@ public class ServerHttpSecurity {
|
|||||||
|
|
||||||
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
||||||
|
|
||||||
|
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
|
||||||
|
|
||||||
private ServerAccessDeniedHandler accessDeniedHandler = new BearerTokenServerAccessDeniedHandler();
|
private ServerAccessDeniedHandler accessDeniedHandler = new BearerTokenServerAccessDeniedHandler();
|
||||||
|
|
||||||
private ServerAuthenticationConverter bearerTokenConverter = new ServerBearerTokenAuthenticationConverter();
|
private ServerAuthenticationConverter bearerTokenConverter = new ServerBearerTokenAuthenticationConverter();
|
||||||
@@ -4186,6 +4189,20 @@ public class ServerHttpSecurity {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the {@link ServerAuthenticationSuccessHandler} to use. The default
|
||||||
|
* is {@link WebFilterChainServerAuthenticationSuccessHandler}
|
||||||
|
* @param authenticationSuccessHandler the
|
||||||
|
* {@link ServerAuthenticationSuccessHandler} to use
|
||||||
|
* @return the {@link OAuth2ClientSpec} to customize
|
||||||
|
* @since 7.2
|
||||||
|
*/
|
||||||
|
public OAuth2ResourceServerSpec authenticationSuccessHandler(
|
||||||
|
ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
|
||||||
|
this.authenticationSuccessHandler = authenticationSuccessHandler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the {@link ServerAuthenticationConverter} to use for requests
|
* Configures the {@link ServerAuthenticationConverter} to use for requests
|
||||||
* authenticating with
|
* authenticating with
|
||||||
@@ -4254,6 +4271,7 @@ public class ServerHttpSecurity {
|
|||||||
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(this.authenticationManagerResolver);
|
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(this.authenticationManagerResolver);
|
||||||
oauth2.setServerAuthenticationConverter(this.bearerTokenConverter);
|
oauth2.setServerAuthenticationConverter(this.bearerTokenConverter);
|
||||||
oauth2.setAuthenticationFailureHandler(authenticationFailureHandler());
|
oauth2.setAuthenticationFailureHandler(authenticationFailureHandler());
|
||||||
|
oauth2.setAuthenticationSuccessHandler(authenticationSuccessHandler());
|
||||||
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
|
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||||
}
|
}
|
||||||
else if (this.jwt != null) {
|
else if (this.jwt != null) {
|
||||||
@@ -4313,6 +4331,13 @@ public class ServerHttpSecurity {
|
|||||||
return new ServerAuthenticationEntryPointFailureHandler(this.entryPoint);
|
return new ServerAuthenticationEntryPointFailureHandler(this.entryPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ServerAuthenticationSuccessHandler authenticationSuccessHandler() {
|
||||||
|
if (this.authenticationSuccessHandler != null) {
|
||||||
|
return this.authenticationSuccessHandler;
|
||||||
|
}
|
||||||
|
return new WebFilterChainServerAuthenticationSuccessHandler();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures JWT Resource Server Support
|
* Configures JWT Resource Server Support
|
||||||
*/
|
*/
|
||||||
@@ -4387,6 +4412,7 @@ public class ServerHttpSecurity {
|
|||||||
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
|
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
|
||||||
oauth2.setServerAuthenticationConverter(OAuth2ResourceServerSpec.this.bearerTokenConverter);
|
oauth2.setServerAuthenticationConverter(OAuth2ResourceServerSpec.this.bearerTokenConverter);
|
||||||
oauth2.setAuthenticationFailureHandler(authenticationFailureHandler());
|
oauth2.setAuthenticationFailureHandler(authenticationFailureHandler());
|
||||||
|
oauth2.setAuthenticationSuccessHandler(authenticationSuccessHandler());
|
||||||
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
|
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4519,6 +4545,7 @@ public class ServerHttpSecurity {
|
|||||||
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
|
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
|
||||||
oauth2.setServerAuthenticationConverter(OAuth2ResourceServerSpec.this.bearerTokenConverter);
|
oauth2.setServerAuthenticationConverter(OAuth2ResourceServerSpec.this.bearerTokenConverter);
|
||||||
oauth2.setAuthenticationFailureHandler(authenticationFailureHandler());
|
oauth2.setAuthenticationFailureHandler(authenticationFailureHandler());
|
||||||
|
oauth2.setAuthenticationSuccessHandler(authenticationSuccessHandler());
|
||||||
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
|
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
@@ -20,6 +20,7 @@ import org.springframework.security.authentication.ReactiveAuthenticationManager
|
|||||||
import org.springframework.security.web.server.ServerAuthenticationEntryPoint
|
import org.springframework.security.web.server.ServerAuthenticationEntryPoint
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
|
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
|
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
|
||||||
|
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
|
||||||
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler
|
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler
|
||||||
import org.springframework.web.server.ServerWebExchange
|
import org.springframework.web.server.ServerWebExchange
|
||||||
|
|
||||||
@@ -35,6 +36,8 @@ import org.springframework.web.server.ServerWebExchange
|
|||||||
* @property bearerTokenConverter the [ServerAuthenticationConverter] to use for requests authenticating with
|
* @property bearerTokenConverter the [ServerAuthenticationConverter] to use for requests authenticating with
|
||||||
* Bearer Tokens.
|
* Bearer Tokens.
|
||||||
* @property authenticationManagerResolver the [ReactiveAuthenticationManagerResolver] to use.
|
* @property authenticationManagerResolver the [ReactiveAuthenticationManagerResolver] to use.
|
||||||
|
* @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] to use after
|
||||||
|
* authentication success.
|
||||||
*/
|
*/
|
||||||
@ServerSecurityMarker
|
@ServerSecurityMarker
|
||||||
class ServerOAuth2ResourceServerDsl {
|
class ServerOAuth2ResourceServerDsl {
|
||||||
@@ -43,6 +46,7 @@ class ServerOAuth2ResourceServerDsl {
|
|||||||
var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null
|
var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null
|
||||||
var bearerTokenConverter: ServerAuthenticationConverter? = null
|
var bearerTokenConverter: ServerAuthenticationConverter? = null
|
||||||
var authenticationManagerResolver: ReactiveAuthenticationManagerResolver<ServerWebExchange>? = null
|
var authenticationManagerResolver: ReactiveAuthenticationManagerResolver<ServerWebExchange>? = null
|
||||||
|
var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null
|
||||||
|
|
||||||
private var jwt: ((ServerHttpSecurity.OAuth2ResourceServerSpec.JwtSpec) -> Unit)? = null
|
private var jwt: ((ServerHttpSecurity.OAuth2ResourceServerSpec.JwtSpec) -> Unit)? = null
|
||||||
private var opaqueToken: ((ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit)? = null
|
private var opaqueToken: ((ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit)? = null
|
||||||
@@ -115,6 +119,7 @@ class ServerOAuth2ResourceServerDsl {
|
|||||||
authenticationEntryPoint?.also { oauth2ResourceServer.authenticationEntryPoint(authenticationEntryPoint) }
|
authenticationEntryPoint?.also { oauth2ResourceServer.authenticationEntryPoint(authenticationEntryPoint) }
|
||||||
bearerTokenConverter?.also { oauth2ResourceServer.bearerTokenConverter(bearerTokenConverter) }
|
bearerTokenConverter?.also { oauth2ResourceServer.bearerTokenConverter(bearerTokenConverter) }
|
||||||
authenticationManagerResolver?.also { oauth2ResourceServer.authenticationManagerResolver(authenticationManagerResolver!!) }
|
authenticationManagerResolver?.also { oauth2ResourceServer.authenticationManagerResolver(authenticationManagerResolver!!) }
|
||||||
|
authenticationSuccessHandler?.also { oauth2ResourceServer.authenticationSuccessHandler(authenticationSuccessHandler) }
|
||||||
jwt?.also { oauth2ResourceServer.jwt(jwt) }
|
jwt?.also { oauth2ResourceServer.jwt(jwt) }
|
||||||
opaqueToken?.also { oauth2ResourceServer.opaqueToken(opaqueToken) }
|
opaqueToken?.also { oauth2ResourceServer.opaqueToken(opaqueToken) }
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -25,12 +25,12 @@ public class MockSecurityContextHolderStrategy implements SecurityContextHolderS
|
|||||||
private SecurityContext context;
|
private SecurityContext context;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearContext() {
|
public synchronized void clearContext() {
|
||||||
this.context = null;
|
this.context = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SecurityContext getContext() {
|
public synchronized SecurityContext getContext() {
|
||||||
if (this.context == null) {
|
if (this.context == null) {
|
||||||
this.context = createEmptyContext();
|
this.context = createEmptyContext();
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ public class MockSecurityContextHolderStrategy implements SecurityContextHolderS
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setContext(SecurityContext context) {
|
public synchronized void setContext(SecurityContext context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-5
@@ -1527,13 +1527,13 @@ public class PrePostMethodSecurityConfigurationTests {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
@EnableMethodSecurity
|
@EnableMethodSecurity
|
||||||
static class CustomPermissionEvaluatorConfig {
|
static class CustomPermissionEvaluatorConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
|
PermissionEvaluator permissionEvaluator() {
|
||||||
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
|
return new PermissionEvaluator() {
|
||||||
expressionHandler.setPermissionEvaluator(new PermissionEvaluator() {
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasPermission(Authentication authentication, Object targetDomainObject,
|
public boolean hasPermission(Authentication authentication, Object targetDomainObject,
|
||||||
Object permission) {
|
Object permission) {
|
||||||
@@ -1545,8 +1545,7 @@ public class PrePostMethodSecurityConfigurationTests {
|
|||||||
Object permission) {
|
Object permission) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
return expressionHandler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-2
@@ -22,6 +22,7 @@ import java.security.interfaces.ECPrivateKey;
|
|||||||
import java.security.interfaces.ECPublicKey;
|
import java.security.interfaces.ECPublicKey;
|
||||||
import java.security.interfaces.RSAPrivateKey;
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
import java.security.interfaces.RSAPublicKey;
|
import java.security.interfaces.RSAPublicKey;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
@@ -30,6 +31,7 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
import com.nimbusds.jose.jwk.ECKey;
|
import com.nimbusds.jose.jwk.ECKey;
|
||||||
import com.nimbusds.jose.jwk.JWK;
|
import com.nimbusds.jose.jwk.JWK;
|
||||||
@@ -47,20 +49,26 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.config.Customizer;
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.ObjectPostProcessor;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.test.SpringTestContext;
|
import org.springframework.security.config.test.SpringTestContext;
|
||||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
import org.springframework.security.oauth2.jose.TestJwks;
|
import org.springframework.security.oauth2.jose.TestJwks;
|
||||||
import org.springframework.security.oauth2.jose.TestKeys;
|
import org.springframework.security.oauth2.jose.TestKeys;
|
||||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||||
|
import org.springframework.security.oauth2.jwt.DPoPProofContext;
|
||||||
|
import org.springframework.security.oauth2.jwt.DPoPProofJwtDecoderFactory;
|
||||||
|
import org.springframework.security.oauth2.jwt.DPoPProofReplayValidator;
|
||||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||||
import org.springframework.security.oauth2.jwt.Jwt;
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationProvider;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -247,8 +255,8 @@ public class DPoPAuthenticationTests {
|
|||||||
)
|
)
|
||||||
.oauth2ResourceServer((oauth2) -> oauth2
|
.oauth2ResourceServer((oauth2) -> oauth2
|
||||||
.jwt(Customizer.withDefaults())
|
.jwt(Customizer.withDefaults())
|
||||||
.dPoP(Customizer.withDefaults()));
|
.dPoP(Customizer.withDefaults())
|
||||||
|
.withObjectPostProcessor(dPoPProofVerifierFactoryCustomizer()));
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
@@ -258,6 +266,25 @@ public class DPoPAuthenticationTests {
|
|||||||
return NimbusJwtDecoder.withPublicKey(PROVIDER_RSA_PUBLIC_KEY).build();
|
return NimbusJwtDecoder.withPublicKey(PROVIDER_RSA_PUBLIC_KEY).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ObjectPostProcessor<DPoPAuthenticationProvider> dPoPProofVerifierFactoryCustomizer() {
|
||||||
|
return new ObjectPostProcessor<>() {
|
||||||
|
@Override
|
||||||
|
public <O extends DPoPAuthenticationProvider> O postProcess(O authenticationProvider) {
|
||||||
|
DPoPProofReplayValidator.InMemoryCache inMemoryCache = new DPoPProofReplayValidator.InMemoryCache();
|
||||||
|
inMemoryCache.setMaxSize(50_000);
|
||||||
|
inMemoryCache.setMaxRequestsPerKey(500);
|
||||||
|
DPoPProofReplayValidator dPoPProofReplayValidator = new DPoPProofReplayValidator(inMemoryCache);
|
||||||
|
dPoPProofReplayValidator.setClockSkew(Duration.ofSeconds(60));
|
||||||
|
Function<DPoPProofContext, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = DPoPProofJwtDecoderFactory
|
||||||
|
.createDefaultJwtValidatorFactory(Collections.singletonList(dPoPProofReplayValidator));
|
||||||
|
DPoPProofJwtDecoderFactory dPoPProofJwtDecoderFactory = new DPoPProofJwtDecoderFactory();
|
||||||
|
dPoPProofJwtDecoderFactory.setJwtValidatorFactory(jwtValidatorFactory);
|
||||||
|
authenticationProvider.setDPoPProofVerifierFactory(dPoPProofJwtDecoderFactory);
|
||||||
|
return authenticationProvider;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
|||||||
+180
@@ -73,9 +73,11 @@ import org.springframework.security.oauth2.server.resource.authentication.Reacti
|
|||||||
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
|
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
|
||||||
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter;
|
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter;
|
||||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
|
import org.springframework.security.web.server.WebFilterExchange;
|
||||||
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
|
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
|
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
|
||||||
|
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||||
import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler;
|
import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -371,6 +373,79 @@ public class OAuth2ResourceServerSpecTests {
|
|||||||
verify(handler).onAuthenticationFailure(any(), any());
|
verify(handler).onAuthenticationFailure(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getWhenUsingCustomAuthenticationSuccessHandlerThenUsesIsAccordingly() {
|
||||||
|
this.spring.register(CustomAuthenticationSuccessHandlerAuthenticationManagerResolverConfig.class).autowire();
|
||||||
|
ServerAuthenticationSuccessHandler handler = this.spring.getContext()
|
||||||
|
.getBean(ServerAuthenticationSuccessHandler.class);
|
||||||
|
ReactiveAuthenticationManager authenticationManager = this.spring.getContext()
|
||||||
|
.getBean(ReactiveAuthenticationManager.class);
|
||||||
|
given(authenticationManager.authenticate(any()))
|
||||||
|
.willAnswer((input) -> Mono.just(input.getArgument(0, Authentication.class)));
|
||||||
|
given(handler.onAuthenticationSuccess(any(), any())).willAnswer((input) -> {
|
||||||
|
WebFilterExchange webFilterExchange = input.getArgument(0, WebFilterExchange.class);
|
||||||
|
return webFilterExchange.getChain().filter(webFilterExchange.getExchange());
|
||||||
|
});
|
||||||
|
// @formatter:off
|
||||||
|
this.client.get()
|
||||||
|
.headers((headers) -> headers.setBearerAuth(this.messageReadToken))
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isUnauthorized();
|
||||||
|
// @formatter:on
|
||||||
|
verify(handler).onAuthenticationSuccess(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getWhenUsingCustomAuthenticationSuccessHandlerWithJwtThenUsesIsAccordingly() {
|
||||||
|
this.spring.register(CustomAuthenticationSuccessHandlerJwtConfig.class).autowire();
|
||||||
|
ServerAuthenticationSuccessHandler handler = this.spring.getContext()
|
||||||
|
.getBean(ServerAuthenticationSuccessHandler.class);
|
||||||
|
ReactiveAuthenticationManager authenticationManager = this.spring.getContext()
|
||||||
|
.getBean(ReactiveAuthenticationManager.class);
|
||||||
|
given(authenticationManager.authenticate(any()))
|
||||||
|
.willAnswer((input) -> Mono.just(input.getArgument(0, Authentication.class)));
|
||||||
|
given(handler.onAuthenticationSuccess(any(), any())).willAnswer((input) -> {
|
||||||
|
WebFilterExchange webFilterExchange = input.getArgument(0, WebFilterExchange.class);
|
||||||
|
return webFilterExchange.getChain().filter(webFilterExchange.getExchange());
|
||||||
|
});
|
||||||
|
// @formatter:off
|
||||||
|
this.client.get()
|
||||||
|
.headers((headers) -> headers.setBearerAuth(this.messageReadToken))
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isUnauthorized();
|
||||||
|
// @formatter:on
|
||||||
|
verify(handler).onAuthenticationSuccess(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getWhenUsingCustomAuthenticationSuccessHandlerWIthOpaqueTokenThenUsesIsAccordingly() {
|
||||||
|
this.spring.register(CustomAuthenticationSuccessHandlerOpaqueTokenConfig.class, RootController.class)
|
||||||
|
.autowire();
|
||||||
|
this.spring.getContext()
|
||||||
|
.getBean(MockWebServer.class)
|
||||||
|
.setDispatcher(requiresAuth(this.clientId, this.clientSecret, this.active));
|
||||||
|
ServerAuthenticationSuccessHandler handler = this.spring.getContext()
|
||||||
|
.getBean(ServerAuthenticationSuccessHandler.class);
|
||||||
|
ReactiveAuthenticationManager authenticationManager = this.spring.getContext()
|
||||||
|
.getBean(ReactiveAuthenticationManager.class);
|
||||||
|
given(authenticationManager.authenticate(any()))
|
||||||
|
.willAnswer((input) -> Mono.just(input.getArgument(0, Authentication.class)));
|
||||||
|
given(handler.onAuthenticationSuccess(any(), any())).willAnswer((input) -> {
|
||||||
|
WebFilterExchange webFilterExchange = input.getArgument(0, WebFilterExchange.class);
|
||||||
|
return webFilterExchange.getChain().filter(webFilterExchange.getExchange());
|
||||||
|
});
|
||||||
|
// @formatter:off
|
||||||
|
this.client.get()
|
||||||
|
.headers((headers) -> headers
|
||||||
|
.setBearerAuth(this.messageReadToken)
|
||||||
|
)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isOk();
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
verify(handler).onAuthenticationSuccess(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void postWhenSignedThenReturnsOk() {
|
public void postWhenSignedThenReturnsOk() {
|
||||||
this.spring.register(PublicKeyConfig.class, RootController.class).autowire();
|
this.spring.register(PublicKeyConfig.class, RootController.class).autowire();
|
||||||
@@ -950,6 +1025,111 @@ public class OAuth2ResourceServerSpecTests {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFlux
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
static class CustomAuthenticationSuccessHandlerAuthenticationManagerResolverConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||||
|
// @formatter:off
|
||||||
|
http
|
||||||
|
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
|
||||||
|
.oauth2ResourceServer((oauth2) -> oauth2
|
||||||
|
.authenticationSuccessHandler(authenticationSuccessHandler())
|
||||||
|
.authenticationManagerResolver((exchange) -> Mono.just(authenticationManager()))
|
||||||
|
);
|
||||||
|
// @formatter:on
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ReactiveAuthenticationManager authenticationManager() {
|
||||||
|
return mock(ReactiveAuthenticationManager.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ServerAuthenticationSuccessHandler authenticationSuccessHandler() {
|
||||||
|
return mock(ServerAuthenticationSuccessHandler.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFlux
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
static class CustomAuthenticationSuccessHandlerJwtConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||||
|
// @formatter:off
|
||||||
|
http
|
||||||
|
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
|
||||||
|
.oauth2ResourceServer((oauth2) -> oauth2
|
||||||
|
.authenticationSuccessHandler(authenticationSuccessHandler())
|
||||||
|
.jwt((jwt) -> jwt.authenticationManager(authenticationManager()))
|
||||||
|
);
|
||||||
|
// @formatter:on
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ReactiveAuthenticationManager authenticationManager() {
|
||||||
|
return mock(ReactiveAuthenticationManager.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ServerAuthenticationSuccessHandler authenticationSuccessHandler() {
|
||||||
|
return mock(ServerAuthenticationSuccessHandler.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFlux
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
static class CustomAuthenticationSuccessHandlerOpaqueTokenConfig {
|
||||||
|
|
||||||
|
private MockWebServer mockWebServer = new MockWebServer();
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||||
|
String introspectionUri = mockWebServer().url("/introspect").toString();
|
||||||
|
// @formatter:off
|
||||||
|
http
|
||||||
|
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
|
||||||
|
.oauth2ResourceServer((oauth2) -> oauth2
|
||||||
|
.authenticationSuccessHandler(authenticationSuccessHandler())
|
||||||
|
.opaqueToken((opaqueToken) -> opaqueToken
|
||||||
|
.introspectionUri(introspectionUri)
|
||||||
|
.introspectionClientCredentials("client", "secret"))
|
||||||
|
);
|
||||||
|
// @formatter:on
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ReactiveAuthenticationManager authenticationManager() {
|
||||||
|
return mock(ReactiveAuthenticationManager.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ServerAuthenticationSuccessHandler authenticationSuccessHandler() {
|
||||||
|
return mock(ServerAuthenticationSuccessHandler.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
MockWebServer mockWebServer() {
|
||||||
|
return this.mockWebServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
void shutdown() throws IOException {
|
||||||
|
this.mockWebServer.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@EnableWebFlux
|
@EnableWebFlux
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
static class CustomBearerTokenServerAuthenticationConverter {
|
static class CustomBearerTokenServerAuthenticationConverter {
|
||||||
|
|||||||
+42
@@ -17,6 +17,7 @@
|
|||||||
package org.springframework.security.config.web.server
|
package org.springframework.security.config.web.server
|
||||||
|
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkObject
|
import io.mockk.mockkObject
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@@ -37,6 +38,7 @@ import org.springframework.security.web.server.SecurityWebFilterChain
|
|||||||
import org.springframework.security.web.server.WebFilterExchange
|
import org.springframework.security.web.server.WebFilterExchange
|
||||||
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint
|
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
|
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
|
||||||
|
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
|
||||||
import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler
|
import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient
|
import org.springframework.test.web.reactive.server.WebTestClient
|
||||||
import org.springframework.web.reactive.config.EnableWebFlux
|
import org.springframework.web.reactive.config.EnableWebFlux
|
||||||
@@ -183,6 +185,46 @@ class ServerOAuth2ResourceServerDslTests {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `request when custom authentication success handler then success handler used`() {
|
||||||
|
this.spring.register(AuthenticationSuccessHandlerConfig::class.java).autowire()
|
||||||
|
every {
|
||||||
|
AuthenticationSuccessHandlerConfig.SUCCESS_HANDLER.onAuthenticationSuccess(any(), any())
|
||||||
|
} returns Mono.empty()
|
||||||
|
|
||||||
|
this.client.get()
|
||||||
|
.uri("/")
|
||||||
|
.headers { it.setBearerAuth(validJwt) }
|
||||||
|
.exchange()
|
||||||
|
|
||||||
|
verify(exactly = 1) { AuthenticationSuccessHandlerConfig.SUCCESS_HANDLER.onAuthenticationSuccess(any(), any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
@EnableWebFlux
|
||||||
|
open class AuthenticationSuccessHandlerConfig {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val SUCCESS_HANDLER: ServerAuthenticationSuccessHandler = mockk()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
|
return http {
|
||||||
|
authorizeExchange {
|
||||||
|
authorize(anyExchange, authenticated)
|
||||||
|
}
|
||||||
|
oauth2ResourceServer {
|
||||||
|
authenticationSuccessHandler = SUCCESS_HANDLER
|
||||||
|
jwt {
|
||||||
|
publicKey = publicKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `request when custom bearer token converter configured then custom converter used`() {
|
fun `request when custom bearer token converter configured then custom converter used`() {
|
||||||
this.spring.register(BearerTokenConverterConfig::class.java).autowire()
|
this.spring.register(BearerTokenConverterConfig::class.java).autowire()
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import org.springframework.security.core.AuthenticationException;
|
|||||||
import org.springframework.security.core.CredentialsContainer;
|
import org.springframework.security.core.CredentialsContainer;
|
||||||
import org.springframework.security.core.SpringSecurityMessageSource;
|
import org.springframework.security.core.SpringSecurityMessageSource;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Iterates an {@link Authentication} request through a list of
|
* Iterates an {@link Authentication} request through a list of
|
||||||
@@ -138,8 +137,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
|
|||||||
private void checkState() {
|
private void checkState() {
|
||||||
Assert.isTrue(this.parent != null || !this.providers.isEmpty(),
|
Assert.isTrue(this.parent != null || !this.providers.isEmpty(),
|
||||||
"A parent AuthenticationManager or a list of AuthenticationProviders is required");
|
"A parent AuthenticationManager or a list of AuthenticationProviders is required");
|
||||||
Assert.isTrue(!CollectionUtils.contains(this.providers.iterator(), null),
|
Assert.noNullElements(this.providers, "providers list cannot contain null values");
|
||||||
"providers list cannot contain null values");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -123,7 +123,7 @@ public final class PostFilterAuthorizationMethodInterceptor implements Authoriza
|
|||||||
/**
|
/**
|
||||||
* Filter a {@code returnedObject} using the {@link PostFilter} annotation that the
|
* Filter a {@code returnedObject} using the {@link PostFilter} annotation that the
|
||||||
* {@link MethodInvocation} specifies.
|
* {@link MethodInvocation} specifies.
|
||||||
* @param mi the {@link MethodInvocation} to check check
|
* @param mi the {@link MethodInvocation} to check
|
||||||
* @return filtered {@code returnedObject}
|
* @return filtered {@code returnedObject}
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+1
-1
@@ -131,7 +131,7 @@ public final class DelegatingSecurityContextCallable<V> implements Callable<V> {
|
|||||||
/**
|
/**
|
||||||
* Creates a {@link DelegatingSecurityContextCallable} and with the given
|
* Creates a {@link DelegatingSecurityContextCallable} and with the given
|
||||||
* {@link Callable} and {@link SecurityContext}, but if the securityContext is null
|
* {@link Callable} and {@link SecurityContext}, but if the securityContext is null
|
||||||
* will defaults to the current {@link SecurityContext} on the
|
* will default to the current {@link SecurityContext} on the
|
||||||
* {@link SecurityContextHolder}
|
* {@link SecurityContextHolder}
|
||||||
* @param delegate the delegate {@link DelegatingSecurityContextCallable} to run with
|
* @param delegate the delegate {@link DelegatingSecurityContextCallable} to run with
|
||||||
* the specified {@link SecurityContext}. Cannot be null.
|
* the specified {@link SecurityContext}. Cannot be null.
|
||||||
|
|||||||
+1
-1
@@ -47,7 +47,7 @@ public final class DelegatingSecurityContextRunnable implements Runnable {
|
|||||||
.getContextHolderStrategy();
|
.getContextHolderStrategy();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link SecurityContext} that the delegate {@link Runnable} will be ran as.
|
* The {@link SecurityContext} that the delegate {@link Runnable} will be run as.
|
||||||
*/
|
*/
|
||||||
private SecurityContext delegateSecurityContext;
|
private SecurityContext delegateSecurityContext;
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -32,6 +32,7 @@ import org.springframework.util.Assert;
|
|||||||
* {@link java.util.ServiceLoader} mechanism when context-propagation is on the classpath.
|
* {@link java.util.ServiceLoader} mechanism when context-propagation is on the classpath.
|
||||||
*
|
*
|
||||||
* @author Steve Riesenberg
|
* @author Steve Riesenberg
|
||||||
|
* @author Tadaya Tsuyukubo
|
||||||
* @since 6.5
|
* @since 6.5
|
||||||
* @see io.micrometer.context.ContextRegistry
|
* @see io.micrometer.context.ContextRegistry
|
||||||
*/
|
*/
|
||||||
@@ -53,7 +54,9 @@ public final class SecurityContextHolderThreadLocalAccessor implements ThreadLoc
|
|||||||
@Override
|
@Override
|
||||||
public void setValue(SecurityContext securityContext) {
|
public void setValue(SecurityContext securityContext) {
|
||||||
Assert.notNull(securityContext, "securityContext cannot be null");
|
Assert.notNull(securityContext, "securityContext cannot be null");
|
||||||
SecurityContextHolder.setContext(securityContext);
|
SecurityContext newContext = SecurityContextHolder.createEmptyContext();
|
||||||
|
newContext.setAuthentication(securityContext.getAuthentication());
|
||||||
|
SecurityContextHolder.setContext(newContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+1
-1
@@ -132,7 +132,7 @@ public class KeyBasedPersistenceTokenService implements TokenService, Initializi
|
|||||||
// Verification
|
// Verification
|
||||||
String content = creationTime + ":" + pseudoRandomNumber + ":" + extendedInfo.toString();
|
String content = creationTime + ":" + pseudoRandomNumber + ":" + extendedInfo.toString();
|
||||||
String expectedSha512Hex = Sha512DigestUtils.shaHex(content + ":" + serverSecret);
|
String expectedSha512Hex = Sha512DigestUtils.shaHex(content + ":" + serverSecret);
|
||||||
Assert.isTrue(expectedSha512Hex.equals(sha1Hex), "Key verification failure");
|
Assert.isTrue(Utf8.isEqual(expectedSha512Hex, sha1Hex), "Key verification failure");
|
||||||
return new DefaultToken(key, creationTime, extendedInfo.toString());
|
return new DefaultToken(key, creationTime, extendedInfo.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import org.jspecify.annotations.Nullable;
|
|||||||
* building block for more sophisticated token-based solutions. For example,
|
* building block for more sophisticated token-based solutions. For example,
|
||||||
* authentication systems that depend on stateless session keys. These could, for
|
* authentication systems that depend on stateless session keys. These could, for
|
||||||
* instance, place the username inside the user-specified extended information associated
|
* instance, place the username inside the user-specified extended information associated
|
||||||
* with the key). It is important to recognise that we do not intend for this interface to
|
* with the key. It is important to recognise that we do not intend for this interface to
|
||||||
* be expanded to provide such capabilities directly.
|
* be expanded to provide such capabilities directly.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ public class User implements UserDetails, CredentialsContainer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the user to be added. At minimum the username, password, and authorities
|
* Builds the user to be added. At minimum the username, password, and authorities
|
||||||
* should provided. The remaining attributes have reasonable defaults.
|
* should be provided. The remaining attributes have reasonable defaults.
|
||||||
*/
|
*/
|
||||||
public static final class UserBuilder {
|
public static final class UserBuilder {
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ public interface UserDetailsService {
|
|||||||
* may possibly be case sensitive, or case insensitive depending on how the
|
* may possibly be case sensitive, or case insensitive depending on how the
|
||||||
* implementation instance is configured. In this case, the <code>UserDetails</code>
|
* implementation instance is configured. In this case, the <code>UserDetails</code>
|
||||||
* object that comes back may have a username that is of a different case than what
|
* object that comes back may have a username that is of a different case than what
|
||||||
* was actually requested..
|
* was actually requested.
|
||||||
* @param username the username identifying the user whose data is required.
|
* @param username the username identifying the user whose data is required.
|
||||||
* @return a fully populated user record (never <code>null</code>)
|
* @return a fully populated user record (never <code>null</code>)
|
||||||
* @throws UsernameNotFoundException if the user could not be found or the user has no
|
* @throws UsernameNotFoundException if the user could not be found or the user has no
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains simple user and authority group account provisioning interfaces together with
|
* Contains simple user and authority group account provisioning interfaces together with
|
||||||
* a a JDBC-based implementation.
|
* a JDBC-based implementation.
|
||||||
*/
|
*/
|
||||||
@NullMarked
|
@NullMarked
|
||||||
package org.springframework.security.provisioning;
|
package org.springframework.security.provisioning;
|
||||||
|
|||||||
+6
-1
@@ -259,7 +259,9 @@ public final class InetAddressMatchers {
|
|||||||
if (address.isLoopbackAddress() || address.isLinkLocalAddress() || address.isSiteLocalAddress()) {
|
if (address.isLoopbackAddress() || address.isLinkLocalAddress() || address.isSiteLocalAddress()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (address.isAnyLocalAddress()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
byte[] rawAddress = address.getAddress();
|
byte[] rawAddress = address.getAddress();
|
||||||
|
|
||||||
if (rawAddress.length == 16) {
|
if (rawAddress.length == 16) {
|
||||||
@@ -335,6 +337,9 @@ public final class InetAddressMatchers {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean matches(@Nullable InetAddress address) {
|
public boolean matches(@Nullable InetAddress address) {
|
||||||
|
if (address == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return !this.internalMatcher.matches(address);
|
return !this.internalMatcher.matches(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+49
-2
@@ -16,11 +16,18 @@
|
|||||||
|
|
||||||
package org.springframework.security.core.context;
|
package org.springframework.security.core.context;
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
@@ -29,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
|||||||
* Tests for {@link SecurityContextHolderThreadLocalAccessor}.
|
* Tests for {@link SecurityContextHolderThreadLocalAccessor}.
|
||||||
*
|
*
|
||||||
* @author Steve Riesenberg
|
* @author Steve Riesenberg
|
||||||
|
* @author Tadaya Tsuyukubo
|
||||||
*/
|
*/
|
||||||
public class SecurityContextHolderThreadLocalAccessorTests {
|
public class SecurityContextHolderThreadLocalAccessorTests {
|
||||||
|
|
||||||
@@ -65,9 +73,11 @@ public class SecurityContextHolderThreadLocalAccessorTests {
|
|||||||
@Test
|
@Test
|
||||||
public void setValueWhenSecurityContextThenSetsSecurityContextHolder() {
|
public void setValueWhenSecurityContextThenSetsSecurityContextHolder() {
|
||||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||||
securityContext.setAuthentication(new TestingAuthenticationToken("user", "password"));
|
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||||
|
securityContext.setAuthentication(authentication);
|
||||||
this.threadLocalAccessor.setValue(securityContext);
|
this.threadLocalAccessor.setValue(securityContext);
|
||||||
assertThat(SecurityContextHolder.getContext()).isSameAs(securityContext);
|
assertThat(SecurityContextHolder.getContext()).isNotSameAs(securityContext);
|
||||||
|
assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(authentication);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -90,4 +100,41 @@ public class SecurityContextHolderThreadLocalAccessorTests {
|
|||||||
assertThat(SecurityContextHolder.getContext()).isEqualTo(emptyContext);
|
assertThat(SecurityContextHolder.getContext()).isEqualTo(emptyContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void newSecurityContextInDifferentThread() throws Exception {
|
||||||
|
Authentication authA = new TestingAuthenticationToken("foo", "password");
|
||||||
|
Authentication authB = new TestingAuthenticationToken("bar", "password");
|
||||||
|
|
||||||
|
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||||
|
securityContext.setAuthentication(authA);
|
||||||
|
SecurityContextHolder.setContext(securityContext);
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
AtomicReference<SecurityContext> contextHolder = new AtomicReference<>();
|
||||||
|
AtomicReference<Authentication> authHolder = new AtomicReference<>();
|
||||||
|
Runnable runnable = () -> {
|
||||||
|
SecurityContext context = SecurityContextHolder.getContext();
|
||||||
|
contextHolder.set(context);
|
||||||
|
authHolder.set(context.getAuthentication());
|
||||||
|
context.setAuthentication(authB);
|
||||||
|
latch.countDown();
|
||||||
|
};
|
||||||
|
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setTaskDecorator(new ContextPropagatingTaskDecorator());
|
||||||
|
executor.afterPropertiesSet();
|
||||||
|
|
||||||
|
executor.execute(runnable);
|
||||||
|
|
||||||
|
boolean finished = latch.await(10, TimeUnit.SECONDS);
|
||||||
|
assertThat(finished).isTrue();
|
||||||
|
|
||||||
|
assertThat(contextHolder.get()).isNotSameAs(securityContext);
|
||||||
|
assertThat(authHolder.get()).isSameAs(authA);
|
||||||
|
|
||||||
|
SecurityContext current = SecurityContextHolder.getContext();
|
||||||
|
assertThat(current).isSameAs(securityContext);
|
||||||
|
assertThat(current.getAuthentication()).isSameAs(authA);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-4
@@ -276,16 +276,17 @@ class InetAddressMatchersTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(strings = { "127.0.0.1", "127.0.0.255" })
|
@ValueSource(strings = { "127.0.0.1", "127.0.0.255", "0.0.0.0" })
|
||||||
void matchesWhenIpv4LoopbackThenReturnsTrue(String address) throws Exception {
|
void matchesWhenIpv4LoopbackThenReturnsTrue(String address) throws Exception {
|
||||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||||
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
|
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
void matchesWhenIpv6LoopbackThenReturnsTrue() throws Exception {
|
@ValueSource(strings = { "::1", "::" })
|
||||||
|
void matchesWhenIpv6LoopbackThenReturnsTrue(String address) throws Exception {
|
||||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||||
assertThat(matcher.matches(InetAddress.getByName("::1"))).isTrue();
|
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@@ -415,6 +416,12 @@ class InetAddressMatchersTests {
|
|||||||
@Nested
|
@Nested
|
||||||
class ExternalInetAddressMatcherTests {
|
class ExternalInetAddressMatcherTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesWhenInetAddressNullThenReturnsFalse() {
|
||||||
|
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
|
||||||
|
assertThat(matcher.matches((InetAddress) null)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(strings = { "8.8.8.8", "1.1.1.1" })
|
@ValueSource(strings = { "8.8.8.8", "1.1.1.1" })
|
||||||
void matchesWhenIpv4PublicThenReturnsTrue(String address) throws Exception {
|
void matchesWhenIpv4PublicThenReturnsTrue(String address) throws Exception {
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import java.nio.CharBuffer;
|
|||||||
import java.nio.charset.CharacterCodingException;
|
import java.nio.charset.CharacterCodingException;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UTF-8 Charset encoder/decoder.
|
* UTF-8 Charset encoder/decoder.
|
||||||
@@ -66,4 +69,21 @@ public final class Utf8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant time comparison to prevent against timing attacks.
|
||||||
|
* @param expected the expected {@link CharSequence}
|
||||||
|
* @param actual the actual {@link CharSequence}
|
||||||
|
* @return true if {@code expected} and {@code actual} are equal, false otherwise
|
||||||
|
* @since 5.7.26
|
||||||
|
*/
|
||||||
|
public static boolean isEqual(@Nullable CharSequence expected, @Nullable CharSequence actual) {
|
||||||
|
byte[] expectedBytes = bytesUtf8(expected);
|
||||||
|
byte[] actualBytes = bytesUtf8(actual);
|
||||||
|
return MessageDigest.isEqual(expectedBytes, actualBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte @Nullable [] bytesUtf8(@Nullable CharSequence s) {
|
||||||
|
return (s != null) ? Utf8.encode(s) : null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -37,7 +37,9 @@ import org.springframework.security.crypto.util.EncodingUtils;
|
|||||||
*
|
*
|
||||||
* @author Keith Donald
|
* @author Keith Donald
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
|
* @deprecated Use {@link AesCbcBytesEncryptor} or {@link AesGcmBytesEncryptor} instead.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public final class AesBytesEncryptor implements BytesEncryptor {
|
public final class AesBytesEncryptor implements BytesEncryptor {
|
||||||
|
|
||||||
private final SecretKey secretKey;
|
private final SecretKey secretKey;
|
||||||
|
|||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
* 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.crypto.encrypt;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.codec.Hex;
|
||||||
|
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
||||||
|
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||||
|
import org.springframework.security.crypto.util.EncodingUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link BytesEncryptor} that uses 256-bit AES/CBC/PKCS5Padding with a random 16-byte
|
||||||
|
* initialization vector. The IV is prepended to the ciphertext on encrypt and stripped on
|
||||||
|
* decrypt.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Note that CBC mode provides confidentiality but <em>not</em> integrity or authenticity.
|
||||||
|
* Applications that require authenticated encryption should prefer
|
||||||
|
* {@link AesGcmBytesEncryptor}. See the <a href=
|
||||||
|
* "https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html">
|
||||||
|
* OWASP Cryptographic Storage Cheat Sheet</a> for guidance on choosing a cipher mode.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* When key derivation is used via {@link #withPassword(String, CharSequence)}, the key is
|
||||||
|
* derived using PBKDF2WithHmacSHA256 with {@code DEFAULT_PBKDF2_ITERATIONS} iterations
|
||||||
|
* per the <a href=
|
||||||
|
* "https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html">
|
||||||
|
* OWASP Password Storage Cheat Sheet</a>. Because derivation is intentionally expensive,
|
||||||
|
* the encryptor instance should be created once and reused rather than constructed
|
||||||
|
* per-operation.
|
||||||
|
*
|
||||||
|
* @author Josh Cummings
|
||||||
|
* @since 5.7.26
|
||||||
|
* @see AesGcmBytesEncryptor
|
||||||
|
* @see AesBytesEncryptor
|
||||||
|
*/
|
||||||
|
public final class AesCbcBytesEncryptor implements BytesEncryptor {
|
||||||
|
|
||||||
|
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
|
||||||
|
|
||||||
|
private static final int IV_LENGTH_BYTES = 16;
|
||||||
|
|
||||||
|
private static final int DEFAULT_PBKDF2_ITERATIONS = 600_000;
|
||||||
|
|
||||||
|
private final SecretKey secretKey;
|
||||||
|
|
||||||
|
private final Cipher encryptor;
|
||||||
|
|
||||||
|
private final Cipher decryptor;
|
||||||
|
|
||||||
|
private final BytesKeyGenerator ivGenerator;
|
||||||
|
|
||||||
|
private AesCbcBytesEncryptor(SecretKey secretKey, BytesKeyGenerator ivGenerator) {
|
||||||
|
this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
|
||||||
|
this.encryptor = CipherUtils.newCipher(ALGORITHM);
|
||||||
|
this.decryptor = CipherUtils.newCipher(ALGORITHM);
|
||||||
|
this.ivGenerator = ivGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an encryptor that derives its key from the given password and hex-encoded
|
||||||
|
* salt using PBKDF2WithHmacSHA1.
|
||||||
|
* @param password the password value
|
||||||
|
* @param salt the hex-encoded salt value
|
||||||
|
*/
|
||||||
|
public static Builder withPassword(String password, CharSequence salt) {
|
||||||
|
return new Builder(password, salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an encryptor using the supplied {@link SecretKey}.
|
||||||
|
* @param secretKey the secret (symmetric) key
|
||||||
|
*/
|
||||||
|
public static Builder withSecretKey(SecretKey secretKey) {
|
||||||
|
return new Builder(secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] encrypt(byte[] bytes) {
|
||||||
|
synchronized (this.encryptor) {
|
||||||
|
byte[] iv = this.ivGenerator.generateKey();
|
||||||
|
CipherUtils.initCipher(this.encryptor, Cipher.ENCRYPT_MODE, this.secretKey, new IvParameterSpec(iv));
|
||||||
|
byte[] ciphertext = CipherUtils.doFinal(this.encryptor, bytes);
|
||||||
|
return EncodingUtils.concatenate(iv, ciphertext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] decrypt(byte[] encryptedBytes) {
|
||||||
|
int ivLength = this.ivGenerator.getKeyLength();
|
||||||
|
byte[] iv = EncodingUtils.subArray(encryptedBytes, 0, ivLength);
|
||||||
|
byte[] ciphertext = EncodingUtils.subArray(encryptedBytes, ivLength, encryptedBytes.length);
|
||||||
|
synchronized (this.decryptor) {
|
||||||
|
CipherUtils.initCipher(this.decryptor, Cipher.DECRYPT_MODE, this.secretKey, new IvParameterSpec(iv));
|
||||||
|
return CipherUtils.doFinal(this.decryptor, ciphertext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SecretKey deriveKey(String password, CharSequence salt) {
|
||||||
|
return CipherUtils.newSecretKey("PBKDF2WithHmacSHA256",
|
||||||
|
new PBEKeySpec(password.toCharArray(), Hex.decode(salt), DEFAULT_PBKDF2_ITERATIONS, 256));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Builder for {@link AesCbcBytesEncryptor}.
|
||||||
|
*/
|
||||||
|
public static final class Builder {
|
||||||
|
|
||||||
|
private final SecretKey secretKey;
|
||||||
|
|
||||||
|
private BytesKeyGenerator ivGenerator = KeyGenerators.secureRandom(IV_LENGTH_BYTES);
|
||||||
|
|
||||||
|
private Builder(SecretKey secretKey) {
|
||||||
|
this.secretKey = secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Builder(String password, CharSequence salt) {
|
||||||
|
this.secretKey = deriveKey(password, salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link BytesKeyGenerator} to use for generating the initialization
|
||||||
|
* vector.
|
||||||
|
* @param ivGenerator the {@link BytesKeyGenerator} to use for generating the
|
||||||
|
* initialization vector
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
|
public Builder ivGenerator(BytesKeyGenerator ivGenerator) {
|
||||||
|
Objects.requireNonNull(ivGenerator, "ivGenerator cannot be null");
|
||||||
|
this.ivGenerator = ivGenerator;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the {@link AesCbcBytesEncryptor}.
|
||||||
|
* @return the {@link AesCbcBytesEncryptor}
|
||||||
|
*/
|
||||||
|
public AesCbcBytesEncryptor build() {
|
||||||
|
return new AesCbcBytesEncryptor(this.secretKey, this.ivGenerator);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
* 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.crypto.encrypt;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.codec.Hex;
|
||||||
|
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
||||||
|
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||||
|
import org.springframework.security.crypto.util.EncodingUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link BytesEncryptor} that uses 256-bit AES/GCM/NoPadding with a random 16-byte
|
||||||
|
* initialization vector and a 128-bit authentication tag. The IV is prepended to the
|
||||||
|
* ciphertext on encrypt and stripped on decrypt. GCM provides authenticated encryption
|
||||||
|
* (AEAD): both confidentiality and integrity are protected, and decryption throws if the
|
||||||
|
* ciphertext has been tampered with.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This class uses a 16-byte (128-bit) IV rather than the 12-byte (96-bit) IV recommended
|
||||||
|
* by NIST SP 800-38D for GCM. Both lengths are cryptographically valid; the 16-byte
|
||||||
|
* choice maintains consistency with the rest of the Spring Security crypto module. For
|
||||||
|
* additional guidance, please see the <a href=
|
||||||
|
* "https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html">
|
||||||
|
* OWASP Cryptographic Storage Cheat Sheet</a>.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* When key derivation is used via {@link #withPassword(String, CharSequence)}, the key is
|
||||||
|
* derived using PBKDF2WithHmacSHA256 with {@code DEFAULT_PBKDF2_ITERATIONS} iterations
|
||||||
|
* per the <a href=
|
||||||
|
* "https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html">
|
||||||
|
* OWASP Password Storage Cheat Sheet</a>. Because derivation is intentionally expensive,
|
||||||
|
* the encryptor instance should be created once and reused rather than constructed
|
||||||
|
* per-operation.
|
||||||
|
*
|
||||||
|
* @author Josh Cummings
|
||||||
|
* @since 5.7.26
|
||||||
|
* @see AesCbcBytesEncryptor
|
||||||
|
* @see AesBytesEncryptor
|
||||||
|
*/
|
||||||
|
public final class AesGcmBytesEncryptor implements BytesEncryptor {
|
||||||
|
|
||||||
|
private static final String ALGORITHM = "AES/GCM/NoPadding";
|
||||||
|
|
||||||
|
private static final int IV_LENGTH_BYTES = 16;
|
||||||
|
|
||||||
|
private static final int TAG_LENGTH_BITS = 128;
|
||||||
|
|
||||||
|
private static final int DEFAULT_PBKDF2_ITERATIONS = 600_000;
|
||||||
|
|
||||||
|
private final SecretKey secretKey;
|
||||||
|
|
||||||
|
private final Cipher encryptor;
|
||||||
|
|
||||||
|
private final Cipher decryptor;
|
||||||
|
|
||||||
|
private final BytesKeyGenerator ivGenerator;
|
||||||
|
|
||||||
|
private AesGcmBytesEncryptor(SecretKey secretKey, BytesKeyGenerator ivGenerator) {
|
||||||
|
this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
|
||||||
|
this.encryptor = CipherUtils.newCipher(ALGORITHM);
|
||||||
|
this.decryptor = CipherUtils.newCipher(ALGORITHM);
|
||||||
|
this.ivGenerator = ivGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an encryptor that derives its key from the given password and hex-encoded
|
||||||
|
* salt using PBKDF2WithHmacSHA1.
|
||||||
|
* @param password the password value
|
||||||
|
* @param salt the hex-encoded salt value
|
||||||
|
*/
|
||||||
|
public static Builder withPassword(String password, CharSequence salt) {
|
||||||
|
return new Builder(password, salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an encryptor using the supplied {@link SecretKey}.
|
||||||
|
* @param secretKey the secret (symmetric) key
|
||||||
|
*/
|
||||||
|
public static Builder withSecretKey(SecretKey secretKey) {
|
||||||
|
return new Builder(secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] encrypt(byte[] bytes) {
|
||||||
|
synchronized (this.encryptor) {
|
||||||
|
byte[] iv = this.ivGenerator.generateKey();
|
||||||
|
CipherUtils.initCipher(this.encryptor, Cipher.ENCRYPT_MODE, this.secretKey,
|
||||||
|
new GCMParameterSpec(TAG_LENGTH_BITS, iv));
|
||||||
|
byte[] ciphertext = CipherUtils.doFinal(this.encryptor, bytes);
|
||||||
|
return EncodingUtils.concatenate(iv, ciphertext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] decrypt(byte[] encryptedBytes) {
|
||||||
|
int ivLength = this.ivGenerator.getKeyLength();
|
||||||
|
byte[] iv = EncodingUtils.subArray(encryptedBytes, 0, ivLength);
|
||||||
|
byte[] ciphertext = EncodingUtils.subArray(encryptedBytes, ivLength, encryptedBytes.length);
|
||||||
|
synchronized (this.decryptor) {
|
||||||
|
CipherUtils.initCipher(this.decryptor, Cipher.DECRYPT_MODE, this.secretKey,
|
||||||
|
new GCMParameterSpec(TAG_LENGTH_BITS, iv));
|
||||||
|
return CipherUtils.doFinal(this.decryptor, ciphertext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SecretKey deriveKey(String password, CharSequence salt) {
|
||||||
|
return CipherUtils.newSecretKey("PBKDF2WithHmacSHA256",
|
||||||
|
new PBEKeySpec(password.toCharArray(), Hex.decode(salt), DEFAULT_PBKDF2_ITERATIONS, 256));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Builder for {@link AesGcmBytesEncryptor}.
|
||||||
|
*/
|
||||||
|
public static final class Builder {
|
||||||
|
|
||||||
|
private final SecretKey secretKey;
|
||||||
|
|
||||||
|
private BytesKeyGenerator ivGenerator = KeyGenerators.secureRandom(IV_LENGTH_BYTES);
|
||||||
|
|
||||||
|
private Builder(SecretKey secretKey) {
|
||||||
|
this.secretKey = secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Builder(String password, CharSequence salt) {
|
||||||
|
this.secretKey = deriveKey(password, salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link BytesKeyGenerator} to use for generating the initialization
|
||||||
|
* vector.
|
||||||
|
* @param ivGenerator the {@link BytesKeyGenerator} to use for generating the
|
||||||
|
* initialization vector
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
|
public Builder ivGenerator(BytesKeyGenerator ivGenerator) {
|
||||||
|
Objects.requireNonNull(ivGenerator, "ivGenerator cannot be null");
|
||||||
|
this.ivGenerator = ivGenerator;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the {@link AesGcmBytesEncryptor}.
|
||||||
|
* @return the {@link AesGcmBytesEncryptor}
|
||||||
|
*/
|
||||||
|
public AesGcmBytesEncryptor build() {
|
||||||
|
return new AesGcmBytesEncryptor(this.secretKey, this.ivGenerator);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package org.springframework.security.crypto.encrypt;
|
package org.springframework.security.crypto.encrypt;
|
||||||
|
|
||||||
import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgorithm;
|
|
||||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,9 +41,14 @@ public final class Encryptors {
|
|||||||
* not be shared
|
* not be shared
|
||||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
||||||
* key
|
* key
|
||||||
|
* @deprecated Use {@link AesGcmBytesEncryptor#withPassword(String, CharSequence)}
|
||||||
|
* instead.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
public static BytesEncryptor stronger(CharSequence password, CharSequence salt) {
|
public static BytesEncryptor stronger(CharSequence password, CharSequence salt) {
|
||||||
return new AesBytesEncryptor(password.toString(), salt, KeyGenerators.secureRandom(16), CipherAlgorithm.GCM);
|
return new AesBytesEncryptor(password.toString(), salt, KeyGenerators.secureRandom(16),
|
||||||
|
AesBytesEncryptor.CipherAlgorithm.GCM);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,18 +57,16 @@ public final class Encryptors {
|
|||||||
* Function #2). Salts the password to prevent dictionary attacks against the key. The
|
* Function #2). Salts the password to prevent dictionary attacks against the key. The
|
||||||
* provided salt is expected to be hex-encoded; it should be random and at least 8
|
* provided salt is expected to be hex-encoded; it should be random and at least 8
|
||||||
* bytes in length. Also applies a random 16-byte initialization vector to ensure each
|
* bytes in length. Also applies a random 16-byte initialization vector to ensure each
|
||||||
* encrypted message will be unique. Requires Java 6. NOTE: This mode is not
|
* encrypted message will be unique. Requires Java 6.
|
||||||
* <a href="https://en.wikipedia.org/wiki/Authenticated_encryption">authenticated</a>
|
|
||||||
* and does not provide any guarantees about the authenticity of the data. For a more
|
|
||||||
* secure alternative, users should prefer
|
|
||||||
* {@link #stronger(CharSequence, CharSequence)}.
|
|
||||||
* @param password the password used to generate the encryptor's secret key; should
|
* @param password the password used to generate the encryptor's secret key; should
|
||||||
* not be shared
|
* not be shared
|
||||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
||||||
* key
|
* key
|
||||||
*
|
* @deprecated Use {@link AesCbcBytesEncryptor#withPassword(String, CharSequence)}
|
||||||
* @see Encryptors#stronger(CharSequence, CharSequence)
|
* instead.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
public static BytesEncryptor standard(CharSequence password, CharSequence salt) {
|
public static BytesEncryptor standard(CharSequence password, CharSequence salt) {
|
||||||
return new AesBytesEncryptor(password.toString(), salt, KeyGenerators.secureRandom(16));
|
return new AesBytesEncryptor(password.toString(), salt, KeyGenerators.secureRandom(16));
|
||||||
}
|
}
|
||||||
@@ -74,8 +76,12 @@ public final class Encryptors {
|
|||||||
* text is hex-encoded.
|
* text is hex-encoded.
|
||||||
* @param password the password used to generate the encryptor's secret key; should
|
* @param password the password used to generate the encryptor's secret key; should
|
||||||
* not be shared
|
* not be shared
|
||||||
|
* @deprecated Use {@link AesGcmBytesEncryptor#withPassword(String, CharSequence)}
|
||||||
|
* instead.
|
||||||
* @see Encryptors#stronger(CharSequence, CharSequence)
|
* @see Encryptors#stronger(CharSequence, CharSequence)
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
public static TextEncryptor delux(CharSequence password, CharSequence salt) {
|
public static TextEncryptor delux(CharSequence password, CharSequence salt) {
|
||||||
return new HexEncodingTextEncryptor(stronger(password, salt));
|
return new HexEncodingTextEncryptor(stronger(password, salt));
|
||||||
}
|
}
|
||||||
@@ -85,8 +91,12 @@ public final class Encryptors {
|
|||||||
* text is hex-encoded.
|
* text is hex-encoded.
|
||||||
* @param password the password used to generate the encryptor's secret key; should
|
* @param password the password used to generate the encryptor's secret key; should
|
||||||
* not be shared
|
* not be shared
|
||||||
|
* @deprecated Use {@link AesCbcBytesEncryptor#withPassword(String, CharSequence)}
|
||||||
|
* instead.
|
||||||
* @see Encryptors#standard(CharSequence, CharSequence)
|
* @see Encryptors#standard(CharSequence, CharSequence)
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
public static TextEncryptor text(CharSequence password, CharSequence salt) {
|
public static TextEncryptor text(CharSequence password, CharSequence salt) {
|
||||||
return new HexEncodingTextEncryptor(standard(password, salt));
|
return new HexEncodingTextEncryptor(standard(password, salt));
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-11
@@ -16,8 +16,6 @@
|
|||||||
|
|
||||||
package org.springframework.security.crypto.password;
|
package org.springframework.security.crypto.password;
|
||||||
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
|
|
||||||
import org.jspecify.annotations.Nullable;
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
import org.springframework.security.crypto.codec.Utf8;
|
import org.springframework.security.crypto.codec.Utf8;
|
||||||
@@ -39,15 +37,7 @@ final class PasswordEncoderUtils {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
static boolean equals(String expected, @Nullable String actual) {
|
static boolean equals(String expected, @Nullable String actual) {
|
||||||
byte[] expectedBytes = bytesUtf8(expected);
|
return Utf8.isEqual(expected, actual);
|
||||||
byte[] actualBytes = bytesUtf8(actual);
|
|
||||||
return MessageDigest.isEqual(expectedBytes, actualBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte @Nullable [] bytesUtf8(@Nullable String s) {
|
|
||||||
// need to check if Utf8.encode() runs in constant time (probably not).
|
|
||||||
// This may leak length of string.
|
|
||||||
return (s != null) ? Utf8.encode(s) : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -24,6 +24,7 @@ import com.password4j.BalloonHashingFunction;
|
|||||||
import com.password4j.Hash;
|
import com.password4j.Hash;
|
||||||
import com.password4j.Password;
|
import com.password4j.Password;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.codec.Utf8;
|
||||||
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
|
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
@@ -140,7 +141,7 @@ public class BalloonHashingPassword4jPasswordEncoder extends AbstractValidatingP
|
|||||||
String expectedHash = parts[1];
|
String expectedHash = parts[1];
|
||||||
|
|
||||||
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.balloonHashingFunction);
|
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.balloonHashingFunction);
|
||||||
return expectedHash.equals(hash.getResult());
|
return Utf8.isEqual(expectedHash, hash.getResult());
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException ex) {
|
catch (IllegalArgumentException ex) {
|
||||||
// Invalid Base64 encoding
|
// Invalid Base64 encoding
|
||||||
|
|||||||
+2
-1
@@ -24,6 +24,7 @@ import com.password4j.Hash;
|
|||||||
import com.password4j.PBKDF2Function;
|
import com.password4j.PBKDF2Function;
|
||||||
import com.password4j.Password;
|
import com.password4j.Password;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.codec.Utf8;
|
||||||
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
|
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
@@ -138,7 +139,7 @@ public class Pbkdf2Password4jPasswordEncoder extends AbstractValidatingPasswordE
|
|||||||
String expectedHash = parts[1];
|
String expectedHash = parts[1];
|
||||||
|
|
||||||
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.pbkdf2Function);
|
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.pbkdf2Function);
|
||||||
return expectedHash.equals(hash.getResult());
|
return Utf8.isEqual(expectedHash, hash.getResult());
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException ex) {
|
catch (IllegalArgumentException ex) {
|
||||||
// Invalid Base64 encoding
|
// Invalid Base64 encoding
|
||||||
|
|||||||
@@ -37,4 +37,48 @@ public class Utf8Tests {
|
|||||||
assertThat(decoded).isEqualTo("6048b75ed560785c");
|
assertThat(decoded).isEqualTo("6048b75ed560785c");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isEqualWhenDifferentLengthThenFalse() {
|
||||||
|
assertThat(Utf8.isEqual("abc", "a")).isFalse();
|
||||||
|
assertThat(Utf8.isEqual("a", "abc")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isEqualWhenNullAndNotEmptyThenFalse() {
|
||||||
|
assertThat(Utf8.isEqual(null, "a")).isFalse();
|
||||||
|
assertThat(Utf8.isEqual("a", null)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isEqualWhenNullAndNullThenTrue() {
|
||||||
|
assertThat(Utf8.isEqual(null, null)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isEqualWhenNullAndEmptyThenFalse() {
|
||||||
|
assertThat(Utf8.isEqual(null, "")).isFalse();
|
||||||
|
assertThat(Utf8.isEqual("", null)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isEqualWhenNotEmptyAndEmptyThenFalse() {
|
||||||
|
assertThat(Utf8.isEqual("abc", "")).isFalse();
|
||||||
|
assertThat(Utf8.isEqual("", "abc")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isEqualWhenEmptyAndEmptyThenTrue() {
|
||||||
|
assertThat(Utf8.isEqual("", "")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isEqualWhenDifferentCaseThenFalse() {
|
||||||
|
assertThat(Utf8.isEqual("aBc", "abc")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isEqualWhenSameThenTrue() {
|
||||||
|
assertThat(Utf8.isEqual("abcdef", "abcdef")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+203
@@ -0,0 +1,203 @@
|
|||||||
|
/*
|
||||||
|
* 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.crypto.encrypt;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.codec.Hex;
|
||||||
|
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
||||||
|
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||||
|
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm;
|
||||||
|
import org.springframework.security.crypto.util.EncodingUtils;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link AesCbcBytesEncryptor}.
|
||||||
|
*/
|
||||||
|
class AesCbcBytesEncryptorTests {
|
||||||
|
|
||||||
|
private final String secret = "value";
|
||||||
|
|
||||||
|
private final String password = "password";
|
||||||
|
|
||||||
|
private final String hexSalt = "deadbeef";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void roundtripWhenUsingPasswordAndSaltThenEncryptsAndDecrypts() {
|
||||||
|
CryptoAssumptions.assumeCBCJCE();
|
||||||
|
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||||
|
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void roundtripWhenUsingSecretKeyThenEncryptsAndDecrypts() {
|
||||||
|
CryptoAssumptions.assumeCBCJCE();
|
||||||
|
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
|
||||||
|
SecretKey secretKey = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
|
||||||
|
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withSecretKey(secretKey).build();
|
||||||
|
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encryptWhenUsingMockIvThenProducesKnownCiphertext() {
|
||||||
|
CryptoAssumptions.assumeCBCJCE();
|
||||||
|
BytesKeyGenerator mockGenerator = mock(BytesKeyGenerator.class);
|
||||||
|
given(mockGenerator.generateKey()).willReturn(Hex.decode("4b0febebd439db7ca77153cb254520c3"));
|
||||||
|
given(mockGenerator.getKeyLength()).willReturn(16);
|
||||||
|
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt)
|
||||||
|
.ivGenerator(mockGenerator)
|
||||||
|
.build();
|
||||||
|
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(Hex.encode(encrypted)).isEqualTo("4b0febebd439db7ca77153cb254520c3b7232ac29355d07869433f1ecf55fe94");
|
||||||
|
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encryptProducesUniqueOutputAndIvIsPrePended() {
|
||||||
|
CryptoAssumptions.assumeCBCJCE();
|
||||||
|
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||||
|
byte[] first = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
byte[] second = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(first).isNotEqualTo(second);
|
||||||
|
assertThat(first.length).isGreaterThan(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
void migratesFromDeprecatedNullIvCbcToAesCbcBytesEncryptor() {
|
||||||
|
CryptoAssumptions.assumeCBCJCE();
|
||||||
|
AesBytesEncryptor deprecated = new AesBytesEncryptor(this.password, this.hexSalt);
|
||||||
|
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
|
||||||
|
|
||||||
|
AesCbcBytesEncryptor modern = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||||
|
BytesEncryptor migrating = new MigratingBytesEncryptor("{CBC}", modern, deprecated);
|
||||||
|
|
||||||
|
assertThat(new String(migrating.decrypt(encrypted))).isEqualTo(this.secret);
|
||||||
|
|
||||||
|
byte[] migrated = migrating.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(migrated[0]).isEqualTo((byte) '{');
|
||||||
|
assertThat(migrated[1]).isEqualTo((byte) 'C');
|
||||||
|
assertThat(migrated[2]).isEqualTo((byte) 'B');
|
||||||
|
assertThat(migrated[3]).isEqualTo((byte) 'C');
|
||||||
|
assertThat(migrated[4]).isEqualTo((byte) '}');
|
||||||
|
assertThat(new String(migrating.decrypt(migrated))).isEqualTo(this.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
void withSecretWhenAesBytesEncryptorEncryptsThenDecrypts() {
|
||||||
|
CryptoAssumptions.assumeCBCJCE();
|
||||||
|
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
|
||||||
|
SecretKey key = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
|
||||||
|
AesBytesEncryptor deprecated = new AesBytesEncryptor(key, KeyGenerators.secureRandom(16),
|
||||||
|
AesBytesEncryptor.CipherAlgorithm.CBC);
|
||||||
|
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withSecretKey(key).build();
|
||||||
|
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
void aesBytesEncryptorWhenEncryptsThenAesCbcBytesEncryptorDecrypts() {
|
||||||
|
CryptoAssumptions.assumeCBCJCE();
|
||||||
|
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
|
||||||
|
SecretKey key = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
|
||||||
|
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withSecretKey(key).build();
|
||||||
|
AesBytesEncryptor deprecated = new AesBytesEncryptor(key, KeyGenerators.secureRandom(16),
|
||||||
|
AesBytesEncryptor.CipherAlgorithm.CBC);
|
||||||
|
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(new String(deprecated.decrypt(encrypted))).isEqualTo(this.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void roundtripWhenUsingCustomIvGeneratorThenEncryptsAndDecrypts() {
|
||||||
|
CryptoAssumptions.assumeCBCJCE();
|
||||||
|
BytesKeyGenerator customIvGenerator = mock(BytesKeyGenerator.class);
|
||||||
|
given(customIvGenerator.generateKey()).willReturn(Hex.decode("4b0febebd439db7ca77153cb254520c3"));
|
||||||
|
given(customIvGenerator.getKeyLength()).willReturn(16);
|
||||||
|
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt)
|
||||||
|
.ivGenerator(customIvGenerator)
|
||||||
|
.build();
|
||||||
|
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
void withPasswordDerivesADifferentKeyThanAesBytesEncryptor() {
|
||||||
|
CryptoAssumptions.assumeCBCJCE();
|
||||||
|
AesBytesEncryptor deprecated = new AesBytesEncryptor(this.password, this.hexSalt,
|
||||||
|
KeyGenerators.secureRandom(16));
|
||||||
|
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||||
|
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encrypted));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MigratingBytesEncryptor implements BytesEncryptor {
|
||||||
|
|
||||||
|
private final byte[] prefix;
|
||||||
|
|
||||||
|
private final BytesEncryptor write;
|
||||||
|
|
||||||
|
private final BytesEncryptor deprecated;
|
||||||
|
|
||||||
|
MigratingBytesEncryptor(String prefix, BytesEncryptor write, BytesEncryptor deprecated) {
|
||||||
|
this.prefix = prefix.getBytes(StandardCharsets.US_ASCII);
|
||||||
|
this.write = write;
|
||||||
|
this.deprecated = deprecated;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] encrypt(byte[] bytes) {
|
||||||
|
return EncodingUtils.concatenate(this.prefix, this.write.encrypt(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] decrypt(byte[] encryptedBytes) {
|
||||||
|
if (startsWith(encryptedBytes, this.prefix)) {
|
||||||
|
byte[] bytes = EncodingUtils.subArray(encryptedBytes, this.prefix.length, encryptedBytes.length);
|
||||||
|
return this.write.decrypt(bytes);
|
||||||
|
}
|
||||||
|
return this.deprecated.decrypt(encryptedBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean startsWith(byte[] data, byte[] prefix) {
|
||||||
|
if (data.length < prefix.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < prefix.length; i++) {
|
||||||
|
if (data[i] != prefix[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
* 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.crypto.encrypt;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.codec.Hex;
|
||||||
|
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
||||||
|
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||||
|
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link AesGcmBytesEncryptor}.
|
||||||
|
*/
|
||||||
|
class AesGcmBytesEncryptorTests {
|
||||||
|
|
||||||
|
private final String secret = "value";
|
||||||
|
|
||||||
|
private final String password = "password";
|
||||||
|
|
||||||
|
private final String hexSalt = "deadbeef";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void roundtripWhenUsingPasswordAndSaltThenEncryptsAndDecrypts() {
|
||||||
|
CryptoAssumptions.assumeGCMJCE();
|
||||||
|
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||||
|
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void roundtripWhenUsingSecretKeyThenEncryptsAndDecrypts() {
|
||||||
|
CryptoAssumptions.assumeGCMJCE();
|
||||||
|
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
|
||||||
|
SecretKey secretKey = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
|
||||||
|
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withSecretKey(secretKey).build();
|
||||||
|
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encryptWhenUsingMockIvThenProducesKnownCiphertext() {
|
||||||
|
CryptoAssumptions.assumeGCMJCE();
|
||||||
|
BytesKeyGenerator mockGenerator = mock(BytesKeyGenerator.class);
|
||||||
|
given(mockGenerator.generateKey()).willReturn(Hex.decode("4b0febebd439db7ca77153cb254520c3"));
|
||||||
|
given(mockGenerator.getKeyLength()).willReturn(16);
|
||||||
|
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt)
|
||||||
|
.ivGenerator(mockGenerator)
|
||||||
|
.build();
|
||||||
|
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(Hex.encode(encrypted))
|
||||||
|
.isEqualTo("4b0febebd439db7ca77153cb254520c3e4d61ae38207b4e42b820d311dc3d4e0e2f37ed5ee");
|
||||||
|
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encryptProducesUniqueOutputAndIvIsPrepended() {
|
||||||
|
CryptoAssumptions.assumeGCMJCE();
|
||||||
|
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||||
|
byte[] first = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
byte[] second = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(first).isNotEqualTo(second);
|
||||||
|
assertThat(first.length).isGreaterThan(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
void withSecretWhenAesBytesEncryptorEncryptsThenDecrypts() {
|
||||||
|
CryptoAssumptions.assumeGCMJCE();
|
||||||
|
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
|
||||||
|
SecretKey key = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
|
||||||
|
AesBytesEncryptor deprecated = new AesBytesEncryptor(key, KeyGenerators.secureRandom(16),
|
||||||
|
AesBytesEncryptor.CipherAlgorithm.GCM);
|
||||||
|
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withSecretKey(key).build();
|
||||||
|
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
void aesBytesEncryptorWhenEncryptsThenAesGcmBytesEncryptorDecrypts() {
|
||||||
|
CryptoAssumptions.assumeGCMJCE();
|
||||||
|
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
|
||||||
|
SecretKey key = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
|
||||||
|
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withSecretKey(key)
|
||||||
|
.ivGenerator(KeyGenerators.secureRandom(12))
|
||||||
|
.build();
|
||||||
|
AesBytesEncryptor deprecated = new AesBytesEncryptor(key, KeyGenerators.secureRandom(12),
|
||||||
|
AesBytesEncryptor.CipherAlgorithm.GCM);
|
||||||
|
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(new String(deprecated.decrypt(encrypted))).isEqualTo(this.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void roundtripWhenUsingCustomIvGeneratorLengthThenEncryptsAndDecrypts() {
|
||||||
|
CryptoAssumptions.assumeGCMJCE();
|
||||||
|
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt)
|
||||||
|
.ivGenerator(KeyGenerators.secureRandom(12))
|
||||||
|
.build();
|
||||||
|
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
void withPasswordDerivesADifferentKeyThanAesBytesEncryptor() {
|
||||||
|
CryptoAssumptions.assumeGCMJCE();
|
||||||
|
AesBytesEncryptor deprecated = new AesBytesEncryptor(this.password, this.hexSalt,
|
||||||
|
KeyGenerators.secureRandom(16), AesBytesEncryptor.CipherAlgorithm.GCM);
|
||||||
|
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||||
|
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encrypted));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void decryptDetectsAuthenticationTagTampering() {
|
||||||
|
CryptoAssumptions.assumeGCMJCE();
|
||||||
|
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||||
|
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||||
|
encrypted[17] ^= 0xFF;
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encrypted));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+4
-6
@@ -24,25 +24,23 @@ import javax.crypto.NoSuchPaddingException;
|
|||||||
import org.junit.jupiter.api.Assumptions;
|
import org.junit.jupiter.api.Assumptions;
|
||||||
import org.opentest4j.TestAbortedException;
|
import org.opentest4j.TestAbortedException;
|
||||||
|
|
||||||
import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgorithm;
|
|
||||||
|
|
||||||
public final class CryptoAssumptions {
|
public final class CryptoAssumptions {
|
||||||
|
|
||||||
private CryptoAssumptions() {
|
private CryptoAssumptions() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void assumeGCMJCE() {
|
public static void assumeGCMJCE() {
|
||||||
assumeAes256(CipherAlgorithm.GCM);
|
assumeAes256("GCM");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void assumeCBCJCE() {
|
public static void assumeCBCJCE() {
|
||||||
assumeAes256(CipherAlgorithm.CBC);
|
assumeAes256("CBC");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assumeAes256(CipherAlgorithm cipherAlgorithm) {
|
private static void assumeAes256(String cipherAlgorithm) {
|
||||||
boolean aes256Available = false;
|
boolean aes256Available = false;
|
||||||
try {
|
try {
|
||||||
Cipher.getInstance(cipherAlgorithm.toString());
|
Cipher.getInstance(cipherAlgorithm);
|
||||||
aes256Available = Cipher.getMaxAllowedKeyLength("AES") >= 256;
|
aes256Available = Cipher.getMaxAllowedKeyLength("AES") >= 256;
|
||||||
}
|
}
|
||||||
catch (NoSuchAlgorithmException ex) {
|
catch (NoSuchAlgorithmException ex) {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ The code is distributed as part of the core module but has no dependencies on an
|
|||||||
|
|
||||||
[[spring-security-crypto-encryption]]
|
[[spring-security-crypto-encryption]]
|
||||||
== Encryptors
|
== Encryptors
|
||||||
The javadoc:org.springframework.security.crypto.encrypt.Encryptors[] class provides factory methods for constructing symmetric encryptors.
|
Spring Security provides javadoc:org.springframework.security.crypto.encrypt.AesGcmBytesEncryptor[] and javadoc:org.springframework.security.crypto.encrypt.AesCbcBytesEncryptor[] for constructing symmetric encryptors.
|
||||||
This class lets you create javadoc:org.springframework.security.crypto.encrypt.BytesEncryptor[] instances to encrypt data in raw `byte[]` form.
|
These can be used to encrypt data in raw `byte[]` form.
|
||||||
You can also construct javadoc:org.springframework.security.crypto.encrypt.TextEncryptor[] instances to encrypt text strings.
|
You can also construct javadoc:org.springframework.security.crypto.encrypt.TextEncryptor[] instances to encrypt text strings.
|
||||||
Encryptors are thread-safe.
|
Encryptors are thread-safe.
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ Both `BytesEncryptor` and `TextEncryptor` are interfaces. `BytesEncryptor` has m
|
|||||||
|
|
||||||
[[spring-security-crypto-encryption-bytes]]
|
[[spring-security-crypto-encryption-bytes]]
|
||||||
=== BytesEncryptor
|
=== BytesEncryptor
|
||||||
You can use the `Encryptors.stronger` factory method to construct a `BytesEncryptor`:
|
Use `AesGcmBytesEncryptor` to construct a BytesEncryptor with authenticated encryption:
|
||||||
|
|
||||||
.BytesEncryptor
|
.BytesEncryptor
|
||||||
[tabs]
|
[tabs]
|
||||||
@@ -29,24 +29,22 @@ Java::
|
|||||||
+
|
+
|
||||||
[source,java,role="primary"]
|
[source,java,role="primary"]
|
||||||
----
|
----
|
||||||
Encryptors.stronger("password", "salt");
|
AesGcmBytesEncryptor.withPassword("password", "salt").build();
|
||||||
----
|
----
|
||||||
|
|
||||||
Kotlin::
|
Kotlin::
|
||||||
+
|
+
|
||||||
[source,kotlin,role="secondary"]
|
[source,kotlin,role="secondary"]
|
||||||
----
|
----
|
||||||
Encryptors.stronger("password", "salt")
|
AesGcmBytesEncryptor.withPassword("password", "salt").build()
|
||||||
----
|
----
|
||||||
======
|
======
|
||||||
|
|
||||||
The `stronger` encryption method creates an encryptor by using 256-bit AES encryption with
|
`AesGcmBytesEncryptor` uses 256-bit AES encryption with Galois Counter Mode (GCM), providing https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated encryption] (AEAD).
|
||||||
Galois Counter Mode (GCM).
|
It derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
|
||||||
It derives the secret key by using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
|
The password used to generate the SecretKey should be kept in a secure place and not be shared.
|
||||||
This method requires Java 6.
|
The salt is used to prevent dictionary attacks against the key in the event your encrypted data is compromised.
|
||||||
The password used to generate the `SecretKey` should be kept in a secure place and should not be shared.
|
A 16-byte random initialization vector is also applied so each encrypted message is unique.
|
||||||
The salt is used to prevent dictionary attacks against the key in the event that your encrypted data is compromised.
|
|
||||||
A 16-byte random initialization vector is also applied so that each encrypted message is unique.
|
|
||||||
|
|
||||||
The provided salt should be in hex-encoded String form, be random, and be at least 8 bytes in length.
|
The provided salt should be in hex-encoded String form, be random, and be at least 8 bytes in length.
|
||||||
You can generate such a salt by using a `KeyGenerator`:
|
You can generate such a salt by using a `KeyGenerator`:
|
||||||
@@ -69,14 +67,33 @@ val salt = KeyGenerators.string().generateKey() // generates a random 8-byte sal
|
|||||||
----
|
----
|
||||||
======
|
======
|
||||||
|
|
||||||
You can also use the `standard` encryption method, which is 256-bit AES in Cipher Block Chaining (CBC) Mode.
|
Users who require CBC mode may use `AesCbcBytesEncryptor`:
|
||||||
|
|
||||||
|
.AesCbcBytesEncryptor
|
||||||
|
[tabs]
|
||||||
|
======
|
||||||
|
Java::
|
||||||
|
+
|
||||||
|
[source,java,role="primary"]
|
||||||
|
----
|
||||||
|
AesCbcBytesEncryptor.withPassword("password", "salt").build();
|
||||||
|
----
|
||||||
|
|
||||||
|
Kotlin::
|
||||||
|
+
|
||||||
|
[source,kotlin,role="secondary"]
|
||||||
|
----
|
||||||
|
AesCbcBytesEncryptor.withPassword("password", "salt").build()
|
||||||
|
----
|
||||||
|
======
|
||||||
|
|
||||||
This mode is not https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated] and does not provide any
|
This mode is not https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated] and does not provide any
|
||||||
guarantees about the authenticity of the data.
|
guarantees about the authenticity of the data.
|
||||||
For a more secure alternative, use `Encryptors.stronger`.
|
For a more secure alternative, users should prefer `AesGcmBytesEncryptor`.
|
||||||
|
|
||||||
[[spring-security-crypto-encryption-text]]
|
[[spring-security-crypto-encryption-text]]
|
||||||
=== TextEncryptor
|
=== TextEncryptor
|
||||||
You can use the `Encryptors.text` factory method to construct a standard TextEncryptor:
|
Use `AesCbcBytesEncryptor` to encrypt text data:
|
||||||
|
|
||||||
.TextEncryptor
|
.TextEncryptor
|
||||||
[tabs]
|
[tabs]
|
||||||
@@ -85,19 +102,22 @@ Java::
|
|||||||
+
|
+
|
||||||
[source,java,role="primary"]
|
[source,java,role="primary"]
|
||||||
----
|
----
|
||||||
Encryptors.text("password", "salt");
|
AesCbcBytesEncryptor.withPassword("password", "salt").build();
|
||||||
----
|
----
|
||||||
|
|
||||||
Kotlin::
|
Kotlin::
|
||||||
+
|
+
|
||||||
[source,kotlin,role="secondary"]
|
[source,kotlin,role="secondary"]
|
||||||
----
|
----
|
||||||
Encryptors.text("password", "salt")
|
AesCbcBytesEncryptor.withPassword("password", "salt").build()
|
||||||
----
|
----
|
||||||
======
|
======
|
||||||
|
|
||||||
A `TextEncryptor` uses a standard `BytesEncryptor` to encrypt text data.
|
`AesCbcBytesEncryptor` encrypts data as raw bytes.
|
||||||
Encrypted results are returned as hex-encoded strings for easy storage on the filesystem or in a database.
|
You can hex-encode the result for easy storage on the filesystem or in the database.
|
||||||
|
|
||||||
|
NOTE: Queryable text encryption (encrypting such that the same plaintext always produces the same ciphertext) is no longer recommended, as it relies on a fixed initialization vector and does not provide adequate security.
|
||||||
|
Instead, look to your data store for a mechanism to query encrypted data.
|
||||||
|
|
||||||
[[spring-security-crypto-keygenerators]]
|
[[spring-security-crypto-keygenerators]]
|
||||||
== Key Generators
|
== Key Generators
|
||||||
|
|||||||
@@ -54,138 +54,13 @@ Therefore, a custom javadoc:org.springframework.security.web.server.authenticati
|
|||||||
One of the most common delivery strategies is a Magic Link, via e-mail, SMS, etc.
|
One of the most common delivery strategies is a Magic Link, via e-mail, SMS, etc.
|
||||||
In the following example, we are going to create a magic link and sent it to the user's email.
|
In the following example, we are going to create a magic link and sent it to the user's email.
|
||||||
|
|
||||||
.One-Time Token Login Configuration
|
include-code::./SecurityConfig[tag=config,indent=0]
|
||||||
[tabs]
|
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
|
||||||
======
|
|
||||||
Java::
|
|
||||||
+
|
|
||||||
[source,java,role="primary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebFluxSecurity
|
|
||||||
public class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
|
||||||
http
|
|
||||||
// ...
|
|
||||||
.formLogin(Customizer.withDefaults())
|
|
||||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
import org.springframework.mail.SimpleMailMessage;
|
|
||||||
import org.springframework.mail.javamail.JavaMailSender;
|
|
||||||
|
|
||||||
@Component <1>
|
|
||||||
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
|
|
||||||
|
|
||||||
private final MailSender mailSender;
|
|
||||||
|
|
||||||
private final ServerOneTimeTokenGenerationSuccessHandler redirectHandler = new ServerRedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
|
|
||||||
|
|
||||||
// constructor omitted
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
|
|
||||||
return Mono.just(exchange.getRequest())
|
|
||||||
.map((request) ->
|
|
||||||
UriComponentsBuilder.fromUri(request.getURI())
|
|
||||||
.replacePath(request.getPath().contextPath().value())
|
|
||||||
.replaceQuery(null)
|
|
||||||
.fragment(null)
|
|
||||||
.path("/login/ott")
|
|
||||||
.queryParam("token", oneTimeToken.getTokenValue())
|
|
||||||
.toUriString() <2>
|
|
||||||
)
|
|
||||||
.flatMap((uri) -> this.mailSender.send(getUserEmail(oneTimeToken.getUsername()), <3>
|
|
||||||
"Use the following link to sign in into the application: " + magicLink)) <4>
|
|
||||||
.then(this.redirectHandler.handle(exchange, oneTimeToken)); <5>
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getUserEmail() {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
class PageController {
|
|
||||||
|
|
||||||
@GetMapping("/ott/sent")
|
|
||||||
String ottSent() {
|
|
||||||
return "my-template";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
Kotlin::
|
|
||||||
+
|
|
||||||
[source,kotlin,role="secondary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebFluxSecurity
|
|
||||||
class SecurityConfig {
|
|
||||||
|
|
||||||
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
|
||||||
return http {
|
|
||||||
authorizeExchange {
|
|
||||||
authorize(anyExchange, authenticated)
|
|
||||||
}
|
|
||||||
oneTimeTokenLogin { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component (1)
|
|
||||||
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
|
|
||||||
|
|
||||||
private val redirectStrategy: ServerRedirectStrategy = DefaultServerRedirectStrategy()
|
|
||||||
|
|
||||||
override fun handle(exchange: ServerWebExchange, oneTimeToken: OneTimeToken): Mono<Void> {
|
|
||||||
val builder = UriComponentsBuilder.fromUri(exchange.request.uri)
|
|
||||||
.replacePath(null)
|
|
||||||
.replaceQuery(null)
|
|
||||||
.fragment(null)
|
|
||||||
.path("/login/ott")
|
|
||||||
.queryParam("token", oneTimeToken.getTokenValue()) (2)
|
|
||||||
val magicLink = builder.toUriString()
|
|
||||||
builder.replacePath(null)
|
|
||||||
.replaceQuery(null)
|
|
||||||
.path("/ott/sent")
|
|
||||||
val redirectLink = builder.toUriString()
|
|
||||||
return this.mailSender.send(
|
|
||||||
getUserEmail(oneTimeToken.getUsername()), (3)
|
|
||||||
"Use the following link to sign in into the application: $magicLink") (4)
|
|
||||||
.then(this.redirectStrategy.sendRedirect(exchange, URI.create(redirectLink))) (5)
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getUserEmail() {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
class PageController {
|
|
||||||
|
|
||||||
@GetMapping("/ott/sent")
|
|
||||||
fun ottSent(): String {
|
|
||||||
return "my-template"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
----
|
|
||||||
======
|
|
||||||
|
|
||||||
<1> Make the `MagicLinkOneTimeTokenGenerationSuccessHandler` a Spring bean
|
<1> Make the `MagicLinkOneTimeTokenGenerationSuccessHandler` a Spring bean
|
||||||
<2> Create a login processing URL with the `token` as a query param
|
<2> Create a login processing URL with the `token` as a query param
|
||||||
<3> Retrieve the user's email based on the username
|
<3> Retrieve the user's email based on the username
|
||||||
<4> Use the `MailSender` API to send the email to the user with the magic link
|
<4> Use the `MailSender` API to send the email to the user with the magic link
|
||||||
<5> Use the `ServerRedirectStrategy` to perform a redirect to your desired URL
|
<5> Use the `ServerOneTimeTokenGenerationSuccessHandler` to perform a redirect to your desired URL
|
||||||
|
|
||||||
The email content will look similar to:
|
The email content will look similar to:
|
||||||
|
|
||||||
@@ -197,65 +72,10 @@ The default submit page will detect that the URL has the `token` query param and
|
|||||||
== Changing the One-Time Token Generate URL
|
== Changing the One-Time Token Generate URL
|
||||||
|
|
||||||
By default, the javadoc:org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter[] listens to `POST /ott/generate` requests.
|
By default, the javadoc:org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter[] listens to `POST /ott/generate` requests.
|
||||||
That URL can be changed by using the `generateTokenUrl(String)` DSL method:
|
That URL can be changed by using the `tokenGeneratingUrl(String)` DSL method:
|
||||||
|
|
||||||
.Changing the Generate URL
|
include-code::./SecurityConfig[tag=config,indent=0]
|
||||||
[tabs]
|
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
|
||||||
======
|
|
||||||
Java::
|
|
||||||
+
|
|
||||||
[source,java,role="primary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebFluxSecurity
|
|
||||||
public class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
|
||||||
http
|
|
||||||
// ...
|
|
||||||
.formLogin(Customizer.withDefaults())
|
|
||||||
.oneTimeTokenLogin((ott) -> ott
|
|
||||||
.generateTokenUrl("/ott/my-generate-url")
|
|
||||||
);
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
Kotlin::
|
|
||||||
+
|
|
||||||
[source,kotlin,role="secondary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebFluxSecurity
|
|
||||||
class SecurityConfig {
|
|
||||||
|
|
||||||
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
|
||||||
return http {
|
|
||||||
// ...
|
|
||||||
formLogin { }
|
|
||||||
oneTimeTokenLogin {
|
|
||||||
generateTokenUrl = "/ott/my-generate-url"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
----
|
|
||||||
======
|
|
||||||
|
|
||||||
[[changing-submit-page-url]]
|
[[changing-submit-page-url]]
|
||||||
== Changing the Default Submit Page URL
|
== Changing the Default Submit Page URL
|
||||||
@@ -263,151 +83,17 @@ class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender):
|
|||||||
The default One-Time Token submit page is generated by the javadoc:org.springframework.security.web.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter[] and listens to `GET /login/ott`.
|
The default One-Time Token submit page is generated by the javadoc:org.springframework.security.web.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter[] and listens to `GET /login/ott`.
|
||||||
The URL can also be changed, like so:
|
The URL can also be changed, like so:
|
||||||
|
|
||||||
.Configuring the Default Submit Page URL
|
include-code::./SecurityConfig[tag=config,indent=0]
|
||||||
[tabs]
|
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
|
||||||
======
|
|
||||||
Java::
|
|
||||||
+
|
|
||||||
[source,java,role="primary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebFluxSecurity
|
|
||||||
public class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
|
||||||
http
|
|
||||||
// ...
|
|
||||||
.formLogin(Customizer.withDefaults())
|
|
||||||
.oneTimeTokenLogin((ott) -> ott
|
|
||||||
.submitPageUrl("/ott/submit")
|
|
||||||
);
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
Kotlin::
|
|
||||||
+
|
|
||||||
[source,kotlin,role="secondary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebFluxSecurity
|
|
||||||
class SecurityConfig {
|
|
||||||
|
|
||||||
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
|
||||||
return http {
|
|
||||||
// ...
|
|
||||||
formLogin { }
|
|
||||||
oneTimeTokenLogin {
|
|
||||||
submitPageUrl = "/ott/submit"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
----
|
|
||||||
======
|
|
||||||
|
|
||||||
[[disabling-default-submit-page]]
|
[[disabling-default-submit-page]]
|
||||||
== Disabling the Default Submit Page
|
== Disabling the Default Submit Page
|
||||||
|
|
||||||
If you want to use your own One-Time Token submit page, you can disable the default page and then provide your own endpoint.
|
If you want to use your own One-Time Token submit page, you can disable the default page and then provide your own endpoint.
|
||||||
|
|
||||||
.Disabling the Default Submit Page
|
include-code::./SecurityConfig[tag=config,indent=0]
|
||||||
[tabs]
|
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
|
||||||
======
|
include-code::./MyController[tag=snippet,indent=0]
|
||||||
Java::
|
|
||||||
+
|
|
||||||
[source,java,role="primary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebFluxSecurity
|
|
||||||
public class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
|
||||||
http
|
|
||||||
.authorizeExchange((authorize) -> authorize
|
|
||||||
.pathMatchers("/my-ott-submit").permitAll()
|
|
||||||
.anyExchange().authenticated()
|
|
||||||
)
|
|
||||||
.formLogin(Customizer.withDefaults())
|
|
||||||
.oneTimeTokenLogin((ott) -> ott
|
|
||||||
.showDefaultSubmitPage(false)
|
|
||||||
);
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
public class MyController {
|
|
||||||
|
|
||||||
@GetMapping("/my-ott-submit")
|
|
||||||
public String ottSubmitPage() {
|
|
||||||
return "my-ott-submit";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
Kotlin::
|
|
||||||
+
|
|
||||||
[source,kotlin,role="secondary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebFluxSecurity
|
|
||||||
class SecurityConfig {
|
|
||||||
|
|
||||||
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
|
||||||
return http {
|
|
||||||
authorizeExchange {
|
|
||||||
authorize(pathMatchers("/my-ott-submit"), permitAll)
|
|
||||||
authorize(anyExchange, authenticated)
|
|
||||||
}
|
|
||||||
.formLogin { }
|
|
||||||
oneTimeTokenLogin {
|
|
||||||
showDefaultSubmitPage = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
class MyController {
|
|
||||||
|
|
||||||
@GetMapping("/my-ott-submit")
|
|
||||||
fun ottSubmitPage(): String {
|
|
||||||
return "my-ott-submit"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
----
|
|
||||||
======
|
|
||||||
|
|
||||||
[[customize-generate-consume-token]]
|
[[customize-generate-consume-token]]
|
||||||
== Customize How to Generate and Consume One-Time Tokens
|
== Customize How to Generate and Consume One-Time Tokens
|
||||||
@@ -425,160 +111,21 @@ Some of the most common reasons to customize the `ReactiveOneTimeTokenService` a
|
|||||||
There are two options to customize the `ReactiveOneTimeTokenService`.
|
There are two options to customize the `ReactiveOneTimeTokenService`.
|
||||||
One option is to provide it as a bean, so it can be automatically be picked-up by the `oneTimeTokenLogin()` DSL:
|
One option is to provide it as a bean, so it can be automatically be picked-up by the `oneTimeTokenLogin()` DSL:
|
||||||
|
|
||||||
.Passing the ReactiveOneTimeTokenService as a Bean
|
include-code::./OneTimeTokenServiceBeanSecurityConfig[tag=config,indent=0]
|
||||||
[tabs]
|
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
|
||||||
======
|
|
||||||
Java::
|
|
||||||
+
|
|
||||||
[source,java,role="primary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebFluxSecurity
|
|
||||||
public class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
|
||||||
http
|
|
||||||
// ...
|
|
||||||
.formLogin(Customizer.withDefaults())
|
|
||||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public ReactiveOneTimeTokenService oneTimeTokenService() {
|
|
||||||
return new MyCustomReactiveOneTimeTokenService();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
Kotlin::
|
|
||||||
+
|
|
||||||
[source,kotlin,role="secondary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebFluxSecurity
|
|
||||||
class SecurityConfig {
|
|
||||||
|
|
||||||
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
|
||||||
return http {
|
|
||||||
//..
|
|
||||||
.formLogin { }
|
|
||||||
oneTimeTokenLogin { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
open fun oneTimeTokenService():ReactiveOneTimeTokenService {
|
|
||||||
return MyCustomReactiveOneTimeTokenService();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
----
|
|
||||||
======
|
|
||||||
|
|
||||||
The second option is to pass the `ReactiveOneTimeTokenService` instance to the DSL, which is useful if there are multiple ``SecurityWebFilterChain``s and a different ``ReactiveOneTimeTokenService``s is needed for each of them.
|
The second option is to pass the `ReactiveOneTimeTokenService` instance to the DSL, which is useful if there are multiple ``SecurityWebFilterChain``s and a different ``ReactiveOneTimeTokenService``s is needed for each of them.
|
||||||
|
|
||||||
.Passing the ReactiveOneTimeTokenService using the DSL
|
include-code::./OneTimeTokenServiceDSLSecurityConfig[tag=config,indent=0]
|
||||||
[tabs]
|
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
|
||||||
======
|
|
||||||
Java::
|
|
||||||
+
|
|
||||||
[source,java,role="primary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebFluxSecurity
|
|
||||||
public class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
|
||||||
http
|
|
||||||
// ...
|
|
||||||
.formLogin(Customizer.withDefaults())
|
|
||||||
.oneTimeTokenLogin((ott) -> ott
|
|
||||||
.oneTimeTokenService(new MyCustomReactiveOneTimeTokenService())
|
|
||||||
);
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
Kotlin::
|
|
||||||
+
|
|
||||||
[source,kotlin,role="secondary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebFluxSecurity
|
|
||||||
class SecurityConfig {
|
|
||||||
|
|
||||||
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
|
||||||
return http {
|
|
||||||
//..
|
|
||||||
.formLogin { }
|
|
||||||
oneTimeTokenLogin {
|
|
||||||
oneTimeTokenService = MyCustomReactiveOneTimeTokenService()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
----
|
|
||||||
======
|
|
||||||
|
|
||||||
[[customize-generate-token-request]]
|
[[customize-generate-token-request]]
|
||||||
== Customize GenerateOneTimeTokenRequest Instance
|
== Customize GenerateOneTimeTokenRequest Instance
|
||||||
There are a number of reasons that you may want to adjust an GenerateOneTimeTokenRequest. For example, you may want expiresIn to be set to 10 mins, which Spring Security sets to 5 mins by default.
|
There are a number of reasons that you may want to adjust an GenerateOneTimeTokenRequest. For example, you may want expiresIn to be set to 10 mins, which Spring Security sets to 5 mins by default.
|
||||||
|
|
||||||
You can customize elements of GenerateOneTimeTokenRequest by publishing an ServerGenerateOneTimeTokenRequestResolver as a @Bean, like so:
|
You can customize elements of GenerateOneTimeTokenRequest by publishing an ServerGenerateOneTimeTokenRequestResolver as a `@Bean`, like so:
|
||||||
[tabs]
|
|
||||||
======
|
|
||||||
Java::
|
|
||||||
+
|
|
||||||
[source,java,role="primary"]
|
|
||||||
----
|
|
||||||
@Bean
|
|
||||||
ServerGenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
|
|
||||||
DefaultServerGenerateOneTimeTokenRequestResolver resolver = new DefaultServerGenerateOneTimeTokenRequestResolver();
|
|
||||||
resolver.setExpiresIn(Duration.ofSeconds(600));
|
|
||||||
return resolver;
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
Kotlin::
|
include-code::./SecurityConfig[tag=config,indent=0]
|
||||||
+
|
|
||||||
[source,kotlin,role="secondary"]
|
|
||||||
----
|
|
||||||
@Bean
|
|
||||||
fun generateOneTimeTokenRequestResolver() : ServerGenerateOneTimeTokenRequestResolver {
|
|
||||||
return DefaultServerGenerateOneTimeTokenRequestResolver().apply {
|
|
||||||
this.setExpiresIn(Duration.ofMinutes(10))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----
|
|
||||||
======
|
|
||||||
|
|
||||||
[[validating-account-status]]
|
[[validating-account-status]]
|
||||||
== Validating Account Status
|
== Validating Account Status
|
||||||
|
|||||||
@@ -54,133 +54,12 @@ Therefore, a custom javadoc:org.springframework.security.web.authentication.ott.
|
|||||||
One of the most common delivery strategies is a Magic Link, via e-mail, SMS, etc.
|
One of the most common delivery strategies is a Magic Link, via e-mail, SMS, etc.
|
||||||
In the following example, we are going to create a magic link and sent it to the user's email.
|
In the following example, we are going to create a magic link and sent it to the user's email.
|
||||||
|
|
||||||
.One-Time Token Login Configuration
|
include-code::./SecurityConfig[tag=config,indent=0]
|
||||||
[tabs]
|
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
|
||||||
======
|
|
||||||
Java::
|
|
||||||
+
|
|
||||||
[source,java,role="primary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
public class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
|
||||||
http
|
|
||||||
// ...
|
|
||||||
.formLogin(Customizer.withDefaults())
|
|
||||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
import org.springframework.mail.SimpleMailMessage;
|
|
||||||
import org.springframework.mail.javamail.JavaMailSender;
|
|
||||||
|
|
||||||
@Component <1>
|
|
||||||
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
|
|
||||||
|
|
||||||
private final MailSender mailSender;
|
|
||||||
|
|
||||||
private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
|
|
||||||
|
|
||||||
// constructor omitted
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
|
|
||||||
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(request.getRequestURL().toString())
|
|
||||||
.replacePath(request.getContextPath())
|
|
||||||
.replaceQuery(null)
|
|
||||||
.fragment(null)
|
|
||||||
.path("/login/ott")
|
|
||||||
.queryParam("token", oneTimeToken.getTokenValue()); <2>
|
|
||||||
String magicLink = builder.toUriString();
|
|
||||||
String email = getUserEmail(oneTimeToken.getUsername()); <3>
|
|
||||||
this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: " + magicLink); <4>
|
|
||||||
this.redirectHandler.handle(request, response, oneTimeToken); <5>
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getUserEmail() {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
class PageController {
|
|
||||||
|
|
||||||
@GetMapping("/ott/sent")
|
|
||||||
String ottSent() {
|
|
||||||
return "my-template";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
Kotlin::
|
|
||||||
+
|
|
||||||
[source,kotlin,role="secondary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
|
||||||
http{
|
|
||||||
formLogin {}
|
|
||||||
oneTimeTokenLogin { }
|
|
||||||
}
|
|
||||||
return http.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import org.springframework.mail.SimpleMailMessage;
|
|
||||||
import org.springframework.mail.javamail.JavaMailSender;
|
|
||||||
|
|
||||||
@Component (1)
|
|
||||||
class MagicLinkOneTimeTokenGenerationSuccessHandler(
|
|
||||||
private val mailSender: MailSender,
|
|
||||||
private val redirectHandler: OneTimeTokenGenerationSuccessHandler = RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
|
|
||||||
) : OneTimeTokenGenerationSuccessHandler {
|
|
||||||
|
|
||||||
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
|
|
||||||
val builder = UriComponentsBuilder.fromUriString(request.getRequestURL().toString())
|
|
||||||
.replacePath(request.contextPath)
|
|
||||||
.replaceQuery(null)
|
|
||||||
.fragment(null)
|
|
||||||
.path("/login/ott")
|
|
||||||
.queryParam("token", oneTimeToken.getTokenValue()) (2)
|
|
||||||
val magicLink = builder.toUriString()
|
|
||||||
val email = getUserEmail(oneTimeToken.getUsername()) (3)
|
|
||||||
this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: $magicLink")(4)
|
|
||||||
this.redirectHandler.handle(request, response, oneTimeToken) (5)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getUserEmail(): String {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
class PageController {
|
|
||||||
|
|
||||||
@GetMapping("/ott/sent")
|
|
||||||
fun ottSent(): String {
|
|
||||||
return "my-template"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
----
|
|
||||||
======
|
|
||||||
|
|
||||||
<1> Make the `MagicLinkOneTimeTokenGenerationSuccessHandler` a Spring bean
|
<1> Make the `MagicLinkOneTimeTokenGenerationSuccessHandler` a Spring bean
|
||||||
<2> Create a login processing URL with the `token` as a query param
|
<2> Create a login processing URL with the `token` as a query param
|
||||||
<3> Retrieve the user's email based on the username
|
<3> Retrieve the user's email based on the username
|
||||||
<4> Use the `JavaMailSender` API to send the email to the user with the magic link
|
<4> Use the `MailSender` API to send the email to the user with the magic link
|
||||||
<5> Use the `RedirectOneTimeTokenGenerationSuccessHandler` to perform a redirect to your desired URL
|
<5> Use the `RedirectOneTimeTokenGenerationSuccessHandler` to perform a redirect to your desired URL
|
||||||
|
|
||||||
The email content will look similar to:
|
The email content will look similar to:
|
||||||
@@ -193,70 +72,15 @@ The default submit page will detect that the URL has the `token` query param and
|
|||||||
== Changing the One-Time Token Generate URL
|
== Changing the One-Time Token Generate URL
|
||||||
|
|
||||||
By default, the javadoc:org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter[] listens to `POST /ott/generate` requests.
|
By default, the javadoc:org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter[] listens to `POST /ott/generate` requests.
|
||||||
That URL can be changed by using the `generateTokenUrl(String)` DSL method:
|
That URL can be changed by using the `tokenGeneratingUrl(String)` DSL method:
|
||||||
|
|
||||||
.Changing the Generate URL
|
include-code::./SecurityConfig[tag=config,indent=0]
|
||||||
[tabs]
|
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
|
||||||
======
|
|
||||||
Java::
|
|
||||||
+
|
|
||||||
[source,java,role="primary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
public class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
|
||||||
http
|
|
||||||
// ...
|
|
||||||
.formLogin(Customizer.withDefaults())
|
|
||||||
.oneTimeTokenLogin((ott) -> ott
|
|
||||||
.tokenGeneratingUrl("/ott/my-generate-url")
|
|
||||||
);
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
Kotlin::
|
|
||||||
+
|
|
||||||
[source,kotlin,role="secondary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
|
||||||
http {
|
|
||||||
//...
|
|
||||||
formLogin { }
|
|
||||||
oneTimeTokenLogin {
|
|
||||||
tokenGeneratingUrl = "/ott/my-generate-url"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return http.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
======
|
|
||||||
|
|
||||||
[NOTE]
|
[NOTE]
|
||||||
====
|
====
|
||||||
The URI passed to `generateTokenUrl` is matched literally.
|
The URI passed to `tokenGeneratingUrl` is matched literally.
|
||||||
If you want it to live under your application servlet's base path, include that prefix explicitly — for example, `generateTokenUrl("/api/ott/generate")`.
|
If you want it to live under your application servlet's base path, include that prefix explicitly — for example, `tokenGeneratingUrl("/api/ott/generate")`.
|
||||||
The same applies to `loginProcessingUrl` and `tokenGeneratingUrl` elsewhere on this page.
|
The same applies to `loginProcessingUrl` and `tokenGeneratingUrl` elsewhere on this page.
|
||||||
====
|
====
|
||||||
|
|
||||||
@@ -266,151 +90,17 @@ The same applies to `loginProcessingUrl` and `tokenGeneratingUrl` elsewhere on t
|
|||||||
The default One-Time Token submit page is generated by the javadoc:org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter[] and listens to `GET /login/ott`.
|
The default One-Time Token submit page is generated by the javadoc:org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter[] and listens to `GET /login/ott`.
|
||||||
The URL can also be changed, like so:
|
The URL can also be changed, like so:
|
||||||
|
|
||||||
.Configuring the Default Submit Page URL
|
include-code::./SecurityConfig[tag=config,indent=0]
|
||||||
[tabs]
|
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
|
||||||
======
|
|
||||||
Java::
|
|
||||||
+
|
|
||||||
[source,java,role="primary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
public class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
|
||||||
http
|
|
||||||
// ...
|
|
||||||
.formLogin(Customizer.withDefaults())
|
|
||||||
.oneTimeTokenLogin((ott) -> ott
|
|
||||||
.defaultSubmitPageUrl("/ott/submit")
|
|
||||||
);
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class MagicLinkGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
Kotlin::
|
|
||||||
+
|
|
||||||
[source,kotlin,role="secondary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
|
||||||
http {
|
|
||||||
//...
|
|
||||||
formLogin { }
|
|
||||||
oneTimeTokenLogin {
|
|
||||||
defaultSubmitPageUrl = "/ott/submit"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return http.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
======
|
|
||||||
|
|
||||||
[[disabling-default-submit-page]]
|
[[disabling-default-submit-page]]
|
||||||
== Disabling the Default Submit Page
|
== Disabling the Default Submit Page
|
||||||
|
|
||||||
If you want to use your own One-Time Token submit page, you can disable the default page and then provide your own endpoint.
|
If you want to use your own One-Time Token submit page, you can disable the default page and then provide your own endpoint.
|
||||||
|
|
||||||
.Disabling the Default Submit Page
|
include-code::./SecurityConfig[tag=config,indent=0]
|
||||||
[tabs]
|
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
|
||||||
======
|
include-code::./MyController[tag=snippet,indent=0]
|
||||||
Java::
|
|
||||||
+
|
|
||||||
[source,java,role="primary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
public class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
|
||||||
http
|
|
||||||
.authorizeHttpRequests((authorize) -> authorize
|
|
||||||
.requestMatchers("/my-ott-submit").permitAll()
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
)
|
|
||||||
.formLogin(Customizer.withDefaults())
|
|
||||||
.oneTimeTokenLogin((ott) -> ott
|
|
||||||
.showDefaultSubmitPage(false)
|
|
||||||
);
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
public class MyController {
|
|
||||||
|
|
||||||
@GetMapping("/my-ott-submit")
|
|
||||||
public String ottSubmitPage() {
|
|
||||||
return "my-ott-submit";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class OneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
Kotlin::
|
|
||||||
+
|
|
||||||
[source,kotlin,role="secondary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
|
||||||
http {
|
|
||||||
authorizeHttpRequests {
|
|
||||||
authorize("/my-ott-submit", authenticated)
|
|
||||||
authorize(anyRequest, authenticated)
|
|
||||||
}
|
|
||||||
formLogin { }
|
|
||||||
oneTimeTokenLogin {
|
|
||||||
showDefaultSubmitPage = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return http.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
class MyController {
|
|
||||||
|
|
||||||
@GetMapping("/my-ott-submit")
|
|
||||||
fun ottSubmitPage(): String {
|
|
||||||
return "my-ott-submit"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
======
|
|
||||||
|
|
||||||
[[customize-generate-consume-token]]
|
[[customize-generate-consume-token]]
|
||||||
== Customize How to Generate and Consume One-Time Tokens
|
== Customize How to Generate and Consume One-Time Tokens
|
||||||
@@ -429,164 +119,21 @@ Some of the most common reasons to customize the `OneTimeTokenService` are, but
|
|||||||
There are two options to customize the `OneTimeTokenService`.
|
There are two options to customize the `OneTimeTokenService`.
|
||||||
One option is to provide it as a bean, so it can be automatically be picked-up by the `oneTimeTokenLogin()` DSL:
|
One option is to provide it as a bean, so it can be automatically be picked-up by the `oneTimeTokenLogin()` DSL:
|
||||||
|
|
||||||
.Passing the OneTimeTokenService as a Bean
|
include-code::./OneTimeTokenServiceBeanSecurityConfig[tag=config,indent=0]
|
||||||
[tabs]
|
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
|
||||||
======
|
|
||||||
Java::
|
|
||||||
+
|
|
||||||
[source,java,role="primary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
public class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
|
||||||
http
|
|
||||||
// ...
|
|
||||||
.formLogin(Customizer.withDefaults())
|
|
||||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public OneTimeTokenService oneTimeTokenService() {
|
|
||||||
return new MyCustomOneTimeTokenService();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
Kotlin::
|
|
||||||
+
|
|
||||||
[source,kotlin,role="secondary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
|
||||||
http {
|
|
||||||
//...
|
|
||||||
formLogin { }
|
|
||||||
oneTimeTokenLogin { }
|
|
||||||
}
|
|
||||||
return http.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
open fun oneTimeTokenService(): OneTimeTokenService {
|
|
||||||
return MyCustomOneTimeTokenService()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
======
|
|
||||||
|
|
||||||
The second option is to pass the `OneTimeTokenService` instance to the DSL, which is useful if there are multiple `SecurityFilterChain` and a different `OneTimeTokenService` is needed for each of them.
|
The second option is to pass the `OneTimeTokenService` instance to the DSL, which is useful if there are multiple `SecurityFilterChain` and a different `OneTimeTokenService` is needed for each of them.
|
||||||
|
|
||||||
.Passing the OneTimeTokenService using the DSL
|
include-code::./OneTimeTokenServiceDSLSecurityConfig[tag=config,indent=0]
|
||||||
[tabs]
|
include-code::./MagicLinkOneTimeTokenGenerationSuccessHandler[tag=snippet,indent=0]
|
||||||
======
|
|
||||||
Java::
|
|
||||||
+
|
|
||||||
[source,java,role="primary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
public class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
|
||||||
http
|
|
||||||
// ...
|
|
||||||
.formLogin(Customizer.withDefaults())
|
|
||||||
.oneTimeTokenLogin((ott) -> ott
|
|
||||||
.oneTimeTokenService(new MyCustomOneTimeTokenService())
|
|
||||||
);
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
Kotlin::
|
|
||||||
+
|
|
||||||
[source,kotlin,role="secondary"]
|
|
||||||
----
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
|
||||||
http {
|
|
||||||
//...
|
|
||||||
formLogin { }
|
|
||||||
oneTimeTokenLogin {
|
|
||||||
oneTimeTokenService = MyCustomOneTimeTokenService()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return http.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
----
|
|
||||||
======
|
|
||||||
|
|
||||||
[[customize-generate-token-request]]
|
[[customize-generate-token-request]]
|
||||||
== Customize GenerateOneTimeTokenRequest Instance
|
== Customize GenerateOneTimeTokenRequest Instance
|
||||||
There are a number of reasons that you may want to adjust an GenerateOneTimeTokenRequest. For example, you may want expiresIn to be set to 10 mins, which Spring Security sets to 5 mins by default.
|
There are a number of reasons that you may want to adjust an GenerateOneTimeTokenRequest. For example, you may want expiresIn to be set to 10 mins, which Spring Security sets to 5 mins by default.
|
||||||
|
|
||||||
You can customize elements of GenerateOneTimeTokenRequest by publishing an GenerateOneTimeTokenRequestResolver as a @Bean, like so:
|
You can customize elements of GenerateOneTimeTokenRequest by publishing an GenerateOneTimeTokenRequestResolver as a `@Bean`, like so:
|
||||||
[tabs]
|
|
||||||
======
|
|
||||||
Java::
|
|
||||||
+
|
|
||||||
[source,java,role="primary"]
|
|
||||||
----
|
|
||||||
@Bean
|
|
||||||
GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
|
|
||||||
DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver();
|
|
||||||
return (request) -> {
|
|
||||||
GenerateOneTimeTokenRequest generate = delegate.resolve(request);
|
|
||||||
return new GenerateOneTimeTokenRequest(generate.getUsername(), Duration.ofSeconds(600));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
Kotlin::
|
include-code::./SecurityConfig[tag=config,indent=0]
|
||||||
+
|
|
||||||
[source,kotlin,role="secondary"]
|
|
||||||
----
|
|
||||||
@Bean
|
|
||||||
fun generateRequestResolver() : GenerateOneTimeTokenRequestResolver {
|
|
||||||
return DefaultGenerateOneTimeTokenRequestResolver().apply {
|
|
||||||
this.setExpiresIn(Duration.ofMinutes(10))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----
|
|
||||||
======
|
|
||||||
|
|
||||||
[[validating-account-status]]
|
[[validating-account-status]]
|
||||||
== Validating Account Status
|
== Validating Account Status
|
||||||
|
|||||||
@@ -8,3 +8,8 @@
|
|||||||
== Web
|
== Web
|
||||||
|
|
||||||
* Since Spring Framework's `HttpMethod#valueOf` now normalizes casing, `StrictServerWebExchangeFirewall` no longer detects a non-canonical-case HTTP method (for example, `get` instead of `GET`) as a distinct value; such requests are processed as the canonical method instead of being rejected. Applications with a customized `ServerExchangeRejectedHandler` should be aware it is no longer invoked for this case.
|
* Since Spring Framework's `HttpMethod#valueOf` now normalizes casing, `StrictServerWebExchangeFirewall` no longer detects a non-canonical-case HTTP method (for example, `get` instead of `GET`) as a distinct value; such requests are processed as the canonical method instead of being rejected. Applications with a customized `ServerExchangeRejectedHandler` should be aware it is no longer invoked for this case.
|
||||||
|
|
||||||
|
== OAuth 2.0
|
||||||
|
|
||||||
|
* https://github.com/spring-projects/spring-security/pull/18895[gh-18895] - Add `authenticationSuccessHandler` to the Reactive Resource Server DSL
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"antora": "3.2.0-rc.2",
|
"antora": "3.2.0-rc.3",
|
||||||
"@antora/atlas-extension": "1.0.0-alpha.5",
|
"@antora/atlas-extension": "1.0.0-alpha.5",
|
||||||
"@antora/collector-extension": "1.0.3",
|
"@antora/collector-extension": "1.0.3",
|
||||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ dependencies {
|
|||||||
testImplementation 'org.springframework:spring-websocket'
|
testImplementation 'org.springframework:spring-websocket'
|
||||||
|
|
||||||
testImplementation 'org.springframework:spring-webmvc'
|
testImplementation 'org.springframework:spring-webmvc'
|
||||||
|
testImplementation 'org.springframework:spring-context-support'
|
||||||
testImplementation 'jakarta.servlet:jakarta.servlet-api'
|
testImplementation 'jakarta.servlet:jakarta.servlet-api'
|
||||||
testImplementation 'io.mockk:mockk'
|
testImplementation 'io.mockk:mockk'
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
||||||
|
|||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.reactive.authentication.changinggenerateurl;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken;
|
||||||
|
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
|
||||||
|
/**/ return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.reactive.authentication.changinggenerateurl;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
||||||
|
http
|
||||||
|
// ...
|
||||||
|
.formLogin(Customizer.withDefaults())
|
||||||
|
.oneTimeTokenLogin((ott) -> ott
|
||||||
|
.tokenGeneratingUrl("/ott/my-generate-url")
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.reactive.authentication.changingsubmitpageurl;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken;
|
||||||
|
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
|
||||||
|
/**/ return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.reactive.authentication.changingsubmitpageurl;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
||||||
|
http
|
||||||
|
// ...
|
||||||
|
.formLogin(Customizer.withDefaults())
|
||||||
|
.oneTimeTokenLogin((ott) -> ott
|
||||||
|
.defaultSubmitPageUrl("/ott/submit")
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.reactive.authentication.customizegenerateconsumetoken;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken;
|
||||||
|
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
|
||||||
|
/**/ return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.reactive.authentication.customizegenerateconsumetoken;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken;
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken;
|
||||||
|
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
class MyCustomReactiveOneTimeTokenService implements ReactiveOneTimeTokenService {
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<OneTimeToken> generate(GenerateOneTimeTokenRequest request) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<OneTimeToken> consume(OneTimeTokenAuthenticationToken authenticationToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.reactive.authentication.customizegenerateconsumetoken;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
|
import org.springframework.security.kt.docs.reactive.authentication.customizegenerateconsumetoken.MyCustomReactiveOneTimeTokenService;
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class OneTimeTokenServiceBeanSecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
||||||
|
http
|
||||||
|
// ...
|
||||||
|
.formLogin(Customizer.withDefaults())
|
||||||
|
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ReactiveOneTimeTokenService oneTimeTokenService() {
|
||||||
|
return new MyCustomReactiveOneTimeTokenService();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.reactive.authentication.customizegenerateconsumetoken;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
public class OneTimeTokenServiceDSLSecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
||||||
|
http
|
||||||
|
// ...
|
||||||
|
.formLogin(Customizer.withDefaults())
|
||||||
|
.oneTimeTokenLogin((ott) -> ott
|
||||||
|
.tokenService(new MyCustomReactiveOneTimeTokenService())
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.reactive.authentication.customizegeneratetokenrequest;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver;
|
||||||
|
import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver;
|
||||||
|
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Bean
|
||||||
|
ServerGenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
|
||||||
|
DefaultServerGenerateOneTimeTokenRequestResolver resolver = new DefaultServerGenerateOneTimeTokenRequestResolver();
|
||||||
|
resolver.setExpiresIn(Duration.ofMinutes(10));
|
||||||
|
return resolver;
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
|
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.reactive.authentication.disablingdefaultsubmitpage;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken;
|
||||||
|
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
|
||||||
|
/**/ return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.reactive.authentication.disablingdefaultsubmitpage;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Controller
|
||||||
|
public class MyController {
|
||||||
|
|
||||||
|
@GetMapping("/my-ott-submit")
|
||||||
|
public String ottSubmitPage() {
|
||||||
|
return "my-ott-submit";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.reactive.authentication.disablingdefaultsubmitpage;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
||||||
|
http
|
||||||
|
.authorizeExchange((authorize) -> authorize
|
||||||
|
.pathMatchers("/my-ott-submit").permitAll()
|
||||||
|
.anyExchange().authenticated()
|
||||||
|
)
|
||||||
|
.formLogin(Customizer.withDefaults())
|
||||||
|
.oneTimeTokenLogin((ott) -> ott
|
||||||
|
.showDefaultSubmitPage(false)
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.reactive.authentication.sendingtokentouser;
|
||||||
|
|
||||||
|
import org.springframework.mail.MailSender;
|
||||||
|
import org.springframework.mail.SimpleMailMessage;
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken;
|
||||||
|
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
|
||||||
|
import org.springframework.security.web.server.authentication.ott.ServerRedirectOneTimeTokenGenerationSuccessHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component // <1>
|
||||||
|
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
private final MailSender mailSender;
|
||||||
|
|
||||||
|
private final ServerOneTimeTokenGenerationSuccessHandler redirectHandler = new ServerRedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
|
||||||
|
|
||||||
|
public MagicLinkOneTimeTokenGenerationSuccessHandler(MailSender mailSender) {
|
||||||
|
this.mailSender = mailSender;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
|
||||||
|
|
||||||
|
return Mono.just(exchange.getRequest())
|
||||||
|
.map((request) ->
|
||||||
|
UriComponentsBuilder.fromUri(request.getURI())
|
||||||
|
.replacePath(request.getPath().contextPath().value())
|
||||||
|
.replaceQuery(null)
|
||||||
|
.fragment(null)
|
||||||
|
.path("/login/ott")
|
||||||
|
.queryParam("token", oneTimeToken.getTokenValue())
|
||||||
|
.toUriString() // <2>
|
||||||
|
)
|
||||||
|
.flatMap((uri) -> {
|
||||||
|
|
||||||
|
String email = getUserEmail(oneTimeToken.getUsername()); // <3>
|
||||||
|
SimpleMailMessage message = new SimpleMailMessage();
|
||||||
|
message.setTo(email);
|
||||||
|
message.setSubject("Your Spring Security One Time Token");
|
||||||
|
message.setText("Use the following link to sign in into the application: " + uri);
|
||||||
|
this.mailSender.send(message); // <4>
|
||||||
|
return Mono.empty();
|
||||||
|
})
|
||||||
|
.then(this.redirectHandler.handle(exchange, oneTimeToken)); // <5>
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUserEmail(String username) {
|
||||||
|
/**/ return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
class PageController {
|
||||||
|
|
||||||
|
@GetMapping("/ott/sent")
|
||||||
|
String ottSent() {
|
||||||
|
return "my-template";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.reactive.authentication.sendingtokentouser;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
|
||||||
|
http
|
||||||
|
// ...
|
||||||
|
.formLogin(Customizer.withDefaults())
|
||||||
|
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.servlet.authentication.changinggenerateurl;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken;
|
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.servlet.authentication.changinggenerateurl;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||||
|
http
|
||||||
|
// ...
|
||||||
|
.formLogin(Customizer.withDefaults())
|
||||||
|
.oneTimeTokenLogin((ott) -> ott
|
||||||
|
.tokenGeneratingUrl("/ott/my-generate-url")
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.servlet.authentication.changingsubmitpageurl;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken;
|
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.servlet.authentication.changingsubmitpageurl;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||||
|
http
|
||||||
|
// ...
|
||||||
|
.formLogin(Customizer.withDefaults())
|
||||||
|
.oneTimeTokenLogin((ott) -> ott
|
||||||
|
.defaultSubmitPageUrl("/ott/submit")
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.servlet.authentication.customizegenerateconsumetoken;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken;
|
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.servlet.authentication.customizegenerateconsumetoken;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken;
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken;
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeTokenService;
|
||||||
|
|
||||||
|
class MyCustomOneTimeTokenService implements OneTimeTokenService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable OneTimeToken consume(OneTimeTokenAuthenticationToken authenticationToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.servlet.authentication.customizegenerateconsumetoken;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeTokenService;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class OneTimeTokenServiceBeanSecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||||
|
http
|
||||||
|
// ...
|
||||||
|
.formLogin(Customizer.withDefaults())
|
||||||
|
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OneTimeTokenService oneTimeTokenService() {
|
||||||
|
return new MyCustomOneTimeTokenService();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.servlet.authentication.customizegenerateconsumetoken;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class OneTimeTokenServiceDSLSecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||||
|
http
|
||||||
|
// ...
|
||||||
|
.formLogin(Customizer.withDefaults())
|
||||||
|
.oneTimeTokenLogin((ott) -> ott
|
||||||
|
.tokenService(new MyCustomOneTimeTokenService())
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.servlet.authentication.customizegeneratetokenrequest;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
|
||||||
|
import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver;
|
||||||
|
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Bean
|
||||||
|
GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
|
||||||
|
DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver();
|
||||||
|
return (request) -> {
|
||||||
|
GenerateOneTimeTokenRequest generate = delegate.resolve(request);
|
||||||
|
return new GenerateOneTimeTokenRequest(generate.getUsername(), Duration.ofMinutes(10));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
|
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.servlet.authentication.disablingdefaultsubmitpage;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken;
|
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.servlet.authentication.disablingdefaultsubmitpage;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Controller
|
||||||
|
public class MyController {
|
||||||
|
|
||||||
|
@GetMapping("/my-ott-submit")
|
||||||
|
public String ottSubmitPage() {
|
||||||
|
return "my-ott-submit";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.servlet.authentication.disablingdefaultsubmitpage;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||||
|
http
|
||||||
|
.authorizeHttpRequests((authorize) -> authorize
|
||||||
|
.requestMatchers("/my-ott-submit").permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.formLogin(Customizer.withDefaults())
|
||||||
|
.oneTimeTokenLogin((ott) -> ott
|
||||||
|
.showDefaultSubmitPage(false)
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.servlet.authentication.sendingtokentouser;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.mail.MailSender;
|
||||||
|
import org.springframework.mail.SimpleMailMessage;
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken;
|
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||||
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
|
||||||
|
import org.springframework.security.web.util.UrlUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component // <1>
|
||||||
|
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
private final MailSender mailSender;
|
||||||
|
|
||||||
|
private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
|
||||||
|
|
||||||
|
public MagicLinkOneTimeTokenGenerationSuccessHandler(MailSender mailSender) {
|
||||||
|
this.mailSender = mailSender;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
|
||||||
|
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
|
||||||
|
.replacePath(request.getContextPath())
|
||||||
|
.replaceQuery(null)
|
||||||
|
.fragment(null)
|
||||||
|
.path("/login/ott")
|
||||||
|
.queryParam("token", oneTimeToken.getTokenValue()); // <2>
|
||||||
|
String magicLink = builder.toUriString();
|
||||||
|
String email = getUserEmail(oneTimeToken.getUsername()); // <3>
|
||||||
|
SimpleMailMessage message = new SimpleMailMessage();
|
||||||
|
message.setTo(email);
|
||||||
|
message.setSubject("Your Spring Security One Time Token");
|
||||||
|
message.setText("Use the following link to sign in into the application: " + magicLink);
|
||||||
|
this.mailSender.send(message); // <4>
|
||||||
|
this.redirectHandler.handle(request, response, oneTimeToken); // <5>
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUserEmail(String username) {
|
||||||
|
/**/ return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
class PageController {
|
||||||
|
|
||||||
|
@GetMapping("/ott/sent")
|
||||||
|
String ottSent() {
|
||||||
|
return "my-template";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.servlet.authentication.sendingtokentouser;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||||
|
http
|
||||||
|
// ...
|
||||||
|
.formLogin(Customizer.withDefaults())
|
||||||
|
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.reactive.authentication.changinggenerateurl
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken
|
||||||
|
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.server.ServerWebExchange
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
class MagicLinkOneTimeTokenGenerationSuccessHandler : ServerOneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
override fun handle(exchange: ServerWebExchange, oneTimeToken: OneTimeToken): Mono<Void> {
|
||||||
|
/**/ return Mono.empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.reactive.authentication.changinggenerateurl
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||||
|
import org.springframework.security.config.web.server.invoke
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
open class SecurityConfig {
|
||||||
|
|
||||||
|
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
|
return http {
|
||||||
|
// ...
|
||||||
|
formLogin { }
|
||||||
|
oneTimeTokenLogin {
|
||||||
|
tokenGeneratingUrl = "/ott/my-generate-url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.reactive.authentication.changingsubmitpageurl
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken
|
||||||
|
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.server.ServerWebExchange
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
class MagicLinkOneTimeTokenGenerationSuccessHandler : ServerOneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
override fun handle(exchange: ServerWebExchange, oneTimeToken: OneTimeToken): Mono<Void> {
|
||||||
|
/**/ return Mono.empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.reactive.authentication.changingsubmitpageurl
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||||
|
import org.springframework.security.config.web.server.invoke
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
open class SecurityConfig {
|
||||||
|
|
||||||
|
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
|
return http {
|
||||||
|
// ...
|
||||||
|
formLogin { }
|
||||||
|
oneTimeTokenLogin {
|
||||||
|
defaultSubmitPageUrl = "/ott/submit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.reactive.authentication.customizegenerateconsumetoken
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken
|
||||||
|
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.server.ServerWebExchange
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
class MagicLinkOneTimeTokenGenerationSuccessHandler : ServerOneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
override fun handle(exchange: ServerWebExchange, oneTimeToken: OneTimeToken): Mono<Void> {
|
||||||
|
/**/ return Mono.empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.reactive.authentication.customizegenerateconsumetoken
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken
|
||||||
|
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
class MyCustomReactiveOneTimeTokenService: ReactiveOneTimeTokenService {
|
||||||
|
|
||||||
|
override fun generate(request: GenerateOneTimeTokenRequest): Mono<OneTimeToken> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun consume(authenticationToken: OneTimeTokenAuthenticationToken): Mono<OneTimeToken> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.reactive.authentication.customizegenerateconsumetoken
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||||
|
import org.springframework.security.config.web.server.invoke
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
open class OneTimeTokenServiceBeanSecurityConfig {
|
||||||
|
|
||||||
|
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
|
return http {
|
||||||
|
//..
|
||||||
|
formLogin { }
|
||||||
|
oneTimeTokenLogin { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun oneTimeTokenService(): ReactiveOneTimeTokenService {
|
||||||
|
return MyCustomReactiveOneTimeTokenService()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.reactive.authentication.customizegenerateconsumetoken
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||||
|
import org.springframework.security.config.web.server.invoke
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
open class OneTimeTokenServiceDSLSecurityConfig {
|
||||||
|
|
||||||
|
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
|
return http {
|
||||||
|
//..
|
||||||
|
formLogin { }
|
||||||
|
oneTimeTokenLogin {
|
||||||
|
tokenService = MyCustomReactiveOneTimeTokenService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.reactive.authentication.customizegeneratetokenrequest
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver
|
||||||
|
import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
open class SecurityConfig {
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Bean
|
||||||
|
fun generateOneTimeTokenRequestResolver() : ServerGenerateOneTimeTokenRequestResolver {
|
||||||
|
return DefaultServerGenerateOneTimeTokenRequestResolver().apply {
|
||||||
|
this.setExpiresIn(Duration.ofMinutes(10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
|
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.reactive.authentication.disablingdefaultsubmitpage
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken
|
||||||
|
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.server.ServerWebExchange
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
class MagicLinkOneTimeTokenGenerationSuccessHandler : ServerOneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
override fun handle(exchange: ServerWebExchange, oneTimeToken: OneTimeToken): Mono<Void> {
|
||||||
|
/**/ return Mono.empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// end::snippet[]
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.reactive.authentication.disablingdefaultsubmitpage
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Controller
|
||||||
|
class MyController {
|
||||||
|
|
||||||
|
@GetMapping("/my-ott-submit")
|
||||||
|
fun ottSubmitPage(): String {
|
||||||
|
return "my-ott-submit"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.reactive.authentication.disablingdefaultsubmitpage
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||||
|
import org.springframework.security.config.web.server.invoke
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||||
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
open class SecurityConfig {
|
||||||
|
|
||||||
|
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
|
return http {
|
||||||
|
authorizeExchange {
|
||||||
|
authorize(pathMatchers("/my-ott-submit"), permitAll)
|
||||||
|
authorize(anyExchange, authenticated)
|
||||||
|
}
|
||||||
|
formLogin { }
|
||||||
|
oneTimeTokenLogin {
|
||||||
|
showDefaultSubmitPage = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.reactive.authentication.sendingtokentouser
|
||||||
|
|
||||||
|
import org.springframework.mail.MailSender
|
||||||
|
import org.springframework.mail.SimpleMailMessage
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken
|
||||||
|
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
|
||||||
|
import org.springframework.security.web.server.authentication.ott.ServerRedirectOneTimeTokenGenerationSuccessHandler
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.stereotype.Controller
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.server.ServerWebExchange
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component // <1>
|
||||||
|
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender) : ServerOneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
private val redirectHandler: ServerOneTimeTokenGenerationSuccessHandler = ServerRedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
|
||||||
|
|
||||||
|
override fun handle(exchange: ServerWebExchange, oneTimeToken: OneTimeToken): Mono<Void> {
|
||||||
|
|
||||||
|
return Mono.just(exchange.request)
|
||||||
|
.map(Function { request ->
|
||||||
|
UriComponentsBuilder.fromUri(request.uri)
|
||||||
|
.replacePath(request.path.contextPath().value())
|
||||||
|
.replaceQuery(null)
|
||||||
|
.fragment(null)
|
||||||
|
.path("/login/ott")
|
||||||
|
.queryParam("token", oneTimeToken.getTokenValue())
|
||||||
|
.toUriString() // <2>
|
||||||
|
})
|
||||||
|
.flatMap(Function { uri ->
|
||||||
|
val email = getUserEmail(oneTimeToken.getUsername()) // <3>
|
||||||
|
val message = SimpleMailMessage()
|
||||||
|
message.setTo(email)
|
||||||
|
message.subject = "Your Spring Security One Time Token"
|
||||||
|
message.text = "Use the following link to sign in into the application: $uri"
|
||||||
|
this.mailSender.send(message) // <4>
|
||||||
|
Mono.empty()
|
||||||
|
})
|
||||||
|
.then(this.redirectHandler.handle(exchange, oneTimeToken)) // <5>
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUserEmail(username: String): String {
|
||||||
|
/**/ return username
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
class PageController {
|
||||||
|
|
||||||
|
@GetMapping("/ott/sent")
|
||||||
|
fun ottSent(): String {
|
||||||
|
return "my-template"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.reactive.authentication.sendingtokentouser
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||||
|
import org.springframework.security.config.web.server.invoke
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
open class SecurityConfig {
|
||||||
|
|
||||||
|
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
|
return http {
|
||||||
|
authorizeExchange {
|
||||||
|
authorize(anyExchange, authenticated)
|
||||||
|
}
|
||||||
|
oneTimeTokenLogin { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.servlet.authentication.changinggenerateurl
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken
|
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.servlet.authentication.changinggenerateurl
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.invoke
|
||||||
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
open class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
http {
|
||||||
|
//...
|
||||||
|
formLogin { }
|
||||||
|
oneTimeTokenLogin {
|
||||||
|
tokenGeneratingUrl = "/ott/my-generate-url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return http.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.servlet.authentication.changingsubmitpageurl
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken
|
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.servlet.authentication.changingsubmitpageurl
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.invoke
|
||||||
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
open class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
http {
|
||||||
|
//...
|
||||||
|
formLogin { }
|
||||||
|
oneTimeTokenLogin {
|
||||||
|
defaultSubmitPageUrl = "/ott/submit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return http.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.servlet.authentication.customizegenerateconsumetoken
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken
|
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.servlet.authentication.customizegenerateconsumetoken
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeTokenService
|
||||||
|
|
||||||
|
class MyCustomOneTimeTokenService: OneTimeTokenService {
|
||||||
|
|
||||||
|
override fun generate(request: GenerateOneTimeTokenRequest): OneTimeToken {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun consume(authenticationToken: OneTimeTokenAuthenticationToken): OneTimeToken? {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.servlet.authentication.customizegenerateconsumetoken
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeTokenService
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.invoke
|
||||||
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
open class OneTimeTokenServiceBeanSecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
http {
|
||||||
|
//...
|
||||||
|
formLogin { }
|
||||||
|
oneTimeTokenLogin { }
|
||||||
|
}
|
||||||
|
return http.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun oneTimeTokenService(): OneTimeTokenService {
|
||||||
|
return MyCustomOneTimeTokenService()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.servlet.authentication.customizegenerateconsumetoken
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.invoke
|
||||||
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
open class OneTimeTokenServiceDSLSecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
http {
|
||||||
|
//...
|
||||||
|
formLogin { }
|
||||||
|
oneTimeTokenLogin {
|
||||||
|
tokenService = MyCustomOneTimeTokenService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return http.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.servlet.authentication.customizegeneratetokenrequest
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver
|
||||||
|
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
open class SecurityConfig {
|
||||||
|
|
||||||
|
// tag::config[]
|
||||||
|
@Bean
|
||||||
|
open fun generateRequestResolver() : GenerateOneTimeTokenRequestResolver {
|
||||||
|
return DefaultGenerateOneTimeTokenRequestResolver().apply {
|
||||||
|
this.setExpiresIn(Duration.ofMinutes(10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end::config[]
|
||||||
|
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.servlet.authentication.disablingdefaultsubmitpage
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken
|
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
// tag::snippet[]
|
||||||
|
@Component
|
||||||
|
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
|
||||||
|
|
||||||
|
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// end::snippet[]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user