spring-security-oauth2-authorization-server + angular-auth-oidc-client - spring-security

I am using
"angular-auth-oidc-client": "^13.1.0",
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.2.2</version>
</dependency>
I am using "OpenId Connect" in my App. My app works fine if i use a Keycloak. I like to replace it with spring-security-oauth2-authorization-server. I am facing some problems i can not solve.
I hit my app and then i am redirected to the login UI. I enter User + PW and give Consent. Then i send back to my app again. Here i see the following problem
There is no data in the "this.storagePersistenceService" that matches.
Do you have any Idee what could be wrong
Best regards
G
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
import java.util.UUID;
/**
* #author Joe Grandja
* #since 0.0.1
*/
#Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfigNew {
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("*"));
configuration.setAllowedMethods(List.of("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).cors().configurationSource(corsConfigurationSource()).and().build();
}
// #formatter:off
#Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("medical-share-openid-connect-client-id")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:4200/")
.redirectUri("http://127.0.0.1:4200")
.redirectUri("http://localhost:4200/")
// .redirectUri("http://127.0.0.1:4200/login/oauth2/code/messaging-client-oidc")
// .redirectUri("http://127.0.0.1:4200/authorized")
.scope(OidcScopes.OPENID)
.scope("offline_access")
// .scope("message.read")
// .scope("message.write")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
// Save registered client in db as if in-memory
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(registeredClient);
return registeredClientRepository;
}
// #formatter:on
#Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
#Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
#Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
#Bean
public ProviderSettings providerSettings() {
// return ProviderSettings.builder().issuer("http://auth-server:9000").build();
return ProviderSettings.builder().issuer("http://localhost:8088").build();
}
#Bean
public EmbeddedDatabase embeddedDatabase() {
// #formatter:off
return new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.H2)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql")
.addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
// #formatter:on
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
#EnableWebSecurity
public class DefaultSecurityConfigNew {
// #formatter:off
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin(withDefaults());
return http.build();
}
// #formatter:on
// #formatter:off
#Bean
UserDetailsService users() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("U-294b70ca67df4c018f59950f35314944")
.password("U-294b70ca67df4c018f59950f35314944")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
// #formatter:on
}
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.jwk.RSAKey;
import javax.crypto.SecretKey;
import java.security.KeyPair;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
/**
* #author Joe Grandja
* #since 0.1.0
*/
public final class Jwks {
private Jwks() {
}
public static RSAKey generateRsa() {
KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// #formatter:off
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// #formatter:on
}
public static ECKey generateEc() {
KeyPair keyPair = KeyGeneratorUtils.generateEcKey();
ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
Curve curve = Curve.forECParameterSpec(publicKey.getParams());
// #formatter:off
return new ECKey.Builder(curve, publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// #formatter:on
}
public static OctetSequenceKey generateSecret() {
SecretKey secretKey = KeyGeneratorUtils.generateSecretKey();
// #formatter:off
return new OctetSequenceKey.Builder(secretKey)
.keyID(UUID.randomUUID().toString())
.build();
// #formatter:on
}
}
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.spec.ECFieldFp;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.EllipticCurve;
/**
* #author Joe Grandja
* #since 0.1.0
*/
final class KeyGeneratorUtils {
private KeyGeneratorUtils() {
}
static SecretKey generateSecretKey() {
SecretKey hmacKey;
try {
hmacKey = KeyGenerator.getInstance("HmacSha256").generateKey();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return hmacKey;
}
static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
static KeyPair generateEcKey() {
EllipticCurve ellipticCurve = new EllipticCurve(
new ECFieldFp(
new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951")),
new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291"));
ECPoint ecPoint = new ECPoint(
new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109"));
ECParameterSpec ecParameterSpec = new ECParameterSpec(
ellipticCurve,
ecPoint,
new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"),
1);
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
keyPairGenerator.initialize(ecParameterSpec);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
}
import {NgModule} from '#angular/core';
import {AuthModule, LogLevel} from 'angular-auth-oidc-client';
#NgModule({
imports: [
AuthModule.forRoot({
config: {
configId: 'ms-angular-auth-oidc-client-lib-id',
logLevel: LogLevel.Debug,
historyCleanupOff: false,
authority: 'http://localhost:8088',
redirectUrl: 'http://127.0.0.1:4200' + '/',
postLogoutRedirectUri: 'http://127.0.0.1:4200' + '/',
clientId: 'medical-share-openid-connect-client-id',
scope: 'openid offline_access',
responseType: 'code',
silentRenew: true,
renewTimeBeforeTokenExpiresInSeconds: 30,
ignoreNonceAfterRefresh: true,
useRefreshToken: true,
autoUserInfo: false, // change that as it is in the example for spring security like this
secureRoutes: [
'http://localhost:4200/',
'localhost:4200/',
'127.0.0.1:4200/',
'http://127.0.0.1:4200/',
'http://localhost:4200/',
'http://localhost:8081',
'localhost:8081',
],
},
}),
],
exports: [AuthModule],
})
export class AuthConfigModule {}
09:38:13.475 Navigated to http://localhost:4200/should-login
09:38:13.682 [webpack-dev-server] Live Reloading enabled. index.js:548
09:38:13.875 [DEBUG] ms-angular-auth-oidc-client-lib-id - Did not find any configured route for route http://localhost:8088/.well-known/openid-configuration angular-auth-oidc-client.mjs:160:20
09:38:13.878 [DEBUG] ms-angular-auth-oidc-client-lib-id - Did not find any configured route for route ../../assets/config/config.json angular-auth-oidc-client.mjs:160:20
09:38:13.956 Angular is running in development mode. Call enableProdMode() to enable production mode. core.mjs:24856:16
09:38:13.967 [DEBUG] ms-angular-auth-oidc-client-lib-id - Working with config 'ms-angular-auth-oidc-client-lib-id' using http://localhost:8088 angular-auth-oidc-client.mjs:160:20
09:38:13.968 [DEBUG] ms-angular-auth-oidc-client-lib-id - currentUrl to check auth with: http://localhost:4200/should-login angular-auth-oidc-client.mjs:157:20
09:38:13.969 [DEBUG] ms-angular-auth-oidc-client-lib-id - checkAuth completed - firing events now. isAuthenticated: false angular-auth-oidc-client.mjs:160:20
09:38:13.970 Navigated to http://localhost:8088/oauth2/authorize?client_id=medical-share-openid-connect-client-id&redirect_uri=http%3A%2F%2F127.0.0.1%3A4200%2F&response_type=code&scope=openid%20offline_access&nonce=e9bd6b927b16552d4560ef708d5fd8ca49mFmzaxQ&state=771502df2eae52657faf84e1e9cf3253b3TN7VCvc&code_challenge=0AggJb7Z2ONJmNU7rAqXh9BmKf5YeEcrLlmw9lgoMJE&code_challenge_method=S256
09:38:13.970 [DEBUG] ms-angular-auth-oidc-client-lib-id - BEGIN Authorize OIDC Flow, no auth data angular-auth-oidc-client.mjs:160:20
09:38:13.971 [DEBUG] ms-angular-auth-oidc-client-lib-id - Nonce created. nonce:e9bd6b927b16552d4560ef708d5fd8ca49mFmzaxQ angular-auth-oidc-client.mjs:160:20
09:38:13.971 [DEBUG] ms-angular-auth-oidc-client-lib-id - Authorize created. adding myautostate: 771502df2eae52657faf84e1e9cf3253b3TN7VCvc angular-auth-oidc-client.mjs:160:20
09:38:13.994 downloadable font: download failed (font-family: "sdx-icons" style:normal weight:400 stretch:100 src index:1): status=2152398850 source: http://localhost:4200/sdx-icons-p.woff2
09:38:14.019 [webpack-dev-server] Disconnected! index.js:548
09:38:14.021 [webpack-dev-server] Trying to reconnect... index.js:548
09:38:16.459 Navigated to http://localhost:8088/login
09:38:16.801 [webpack-dev-server] Live Reloading enabled. index.js:548
09:38:16.983 [DEBUG] ms-angular-auth-oidc-client-lib-id - Did not find any configured route for route http://localhost:8088/.well-known/openid-configuration angular-auth-oidc-client.mjs:160:20
09:38:16.988 [DEBUG] ms-angular-auth-oidc-client-lib-id - Did not find any configured route for route ../../assets/config/config.json angular-auth-oidc-client.mjs:160:20
09:38:17.056 Angular is running in development mode. Call enableProdMode() to enable production mode. core.mjs:24856:16
09:38:17.069 ERROR Error: Uncaught (in promise): () => new Error(`could not find matching config for state ${stateParamFromUrl}`)
Angular 19
RxJS 21
ZoneAwarePromise Angular
toPromise RxJS
getEnvironmentConfiguration config.service.ts:25
initAppConfig init.app.config.ts:6
Angular 24
7004 main.ts:14
Webpack 7
core.mjs:6484:12
21.3.22
I was able to make the spring-security-oauth2-authorization-server work with the config below.
The autoUserInfo does not work, so i switched it off.
See defaultSecurityFilterChain, i had to add a .permitAll() otherwise the endpoint for the auth flow where protected. That feels somehow wrong.
RegisteredClient: took the config from the example #SteveRiesenberg mendtiond
#SteveRiesenberg do you see any improvments / simplification possible in that code?
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.UUID;
/**
* #author Joe Grandja
* #since 0.0.1
*/
#Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfigNew {
public static CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
// config.setAllowCredentials(true);
source.registerCorsConfiguration("/**", config);
return source;
}
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).cors().configurationSource(corsConfigurationSource()).and().build();
}
// #formatter:off
#Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient publicClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("medical-share-openid-connect-client-id")
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("http://127.0.0.1:4200")
.redirectUri("http://127.0.0.1:4200/")
.redirectUri("http://127.0.0.1:4200/silent-renew.html")
.scope(OidcScopes.OPENID)
.scope("offline_access")
// .scope("message.read")
// .scope("message.write")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).requireProofKey(true).build())
.build();
// Save registered client in db as if in-memory
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(publicClient);
return registeredClientRepository;
}
// #formatter:on
#Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
#Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
#Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
#Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder().issuer("http://127.0.0.1:8088").build();
// return ProviderSettings.builder().issuer("http://localhost:8088").build();
}
#Bean
public EmbeddedDatabase embeddedDatabase() {
// #formatter:off
return new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.H2)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql")
.addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
// #formatter:on
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static com.test.kubernetes.authorizationserver.application.sample.config.AuthorizationServerConfigNew.corsConfigurationSource;
import static org.springframework.security.config.Customizer.withDefaults;
#EnableWebSecurity
public class DefaultSecurityConfigNew {
// #formatter:off
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.cors().configurationSource(corsConfigurationSource()).and()
.authorizeRequests()
// .antMatchers("/.well-known/**", "/oauth2/**", "/userinfo")
.antMatchers("/**")
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/login")
.authenticated()
.and()
.formLogin(withDefaults());
return http.cors().configurationSource(corsConfigurationSource()).and().build();
}
// #formatter:on
// #formatter:off
#Bean
UserDetailsService users() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("U-294b70ca67df4c018f59950f35314944")
.password("U-294b70ca67df4c018f59950f35314944")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
// #formatter:on
}
import {NgModule} from '#angular/core';
import {AuthModule, LogLevel} from 'angular-auth-oidc-client';
#NgModule({
imports: [
AuthModule.forRoot({
config: {
configId: 'ms-angular-auth-oidc-client-lib-id',
logLevel: LogLevel.Debug,
historyCleanupOff: false,
// authority: 'http://localhost:8088/realms/master',
authority: 'http://127.0.0.1:8088',
redirectUrl: 'http://127.0.0.1:4201/',
// redirectUrl: 'http://localhost:4201/',
postLogoutRedirectUri: 'http://127.0.0.1:4201/',
clientId: 'medical-share-openid-connect-client-id',
scope: 'openid offline_access',
responseType: 'code',
silentRenew: true,
renewTimeBeforeTokenExpiresInSeconds: 10,
autoUserInfo: false,
ignoreNonceAfterRefresh: true,
useRefreshToken: true,
// autoCleanStateAfterAuthentication: false,
secureRoutes: [
'http://localhost:4201/',
'localhost:4201/',
'127.0.0.1:4201/',
'http://127.0.0.1:4201/',
'http://127.0.0.1:4201',
'http://localhost:4201/',
'http://localhost:8083',
'localhost:8083',
'http://127.0.0.1:8088',
'http://127.0.0.1:8088/',
],
},
}),
],
exports: [AuthModule],
})
export class AuthConfigModule {
}

I'll try to provide an answer to the sequence of questions/challenges you're facing.
Based on comments and updates to the question, it looks like you have a few incorrect configurations to address. From our first round of comments:
It looks like you've configured a confidential client (with a client secret, client authentication method, requireProofKey(true) not set
Note: Also, make sure you're browsing your application from http://127.0.0.1:4200, not http://localhost:4200.
From the update you provided in the question:
You don't want to comment out config.setAllowCredentials(true); in your cors config as the browser needs to be able to send the JSESSIONID cookie with the silent renew process.
Spring Authorization Server doesn't currently support refresh tokens for public clients, so my sample doesn't include that option useRefreshToken: true.
Your .authorizeRequests() DSL usage is not correct. You don't want to have multiple invocations of that method, as the 2nd one overwrites the first one. You are also targeting the wrong endpoints in the default filter chain. It should look like the sample:
// #formatter:off
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
// Note: You can also make the corsConfigurationSource() non-static
// and annotated with #Bean. Then use instead:
// .cors(Customizer.withDefaults())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.formLogin(Customizer.withDefaults());
return http.build();
}
// #formatter:on
There may be other issues, it's difficult to tell. I would recommend starting with this working sample (see also the angular-client auth config) and evolving it with any customizations you're thinking of to see if you've introduced an issue.

Finally i made it :-) Thanks a lot to #Steve.
Note that i am aware that there is no RefreshToken support. Therefore i made the lifetime of a token 1 day.
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.RSAKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import java.lang.invoke.MethodHandles;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
#RestController
#RequestMapping()
public class OpenIdConnectMockController {
static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private KeyPair rsaKeyPair;
private RSAKey jwkRsaPublicKey;
#PostConstruct
public void generateKey() {
this.rsaKeyPair = generateRsaKey();
this.jwkRsaPublicKey = generateRsa(this.rsaKeyPair);
}
/**
* Note this method is currently not use, but here for future use.
* We use the same URL as in the Keycloak, so we have not to change things when running the thing against ms-backend-test-openid-connect-mock or Keycloak.
*/
#GetMapping(path = "/realms/master/protocol/openid-connect/certs", produces = "application/json")
public String keys() {
logger.info("Keys was called {}", this.jwkRsaPublicKey.toString());
return "{\"keys\":[" + this.jwkRsaPublicKey.toString() + "]}";
}
/**
* This is used in the SystemTest. There we download the private key and sign the JWT.
*/
#GetMapping(path = "/realms/master/protocol/openid-connect/private-key", produces = "application/json")
public byte[] getPrivateKey() throws JOSEException {
RSAKey privateKey = new RSAKey.Builder((RSAPublicKey) this.rsaKeyPair.getPublic()).privateKey(this.rsaKeyPair.getPrivate()).build();
return privateKey.toRSAPrivateKey().getEncoded();
}
public RSAKey getJwkRsaPublicKey() {
return this.jwkRsaPublicKey;
}
private RSAKey generateRsa(KeyPair keyPair) {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
private KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
#Configuration
public class PersistedUserConfig {
#Bean
UserDetailsService users() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("U-294b70ca67df4c018f59950f35314944")
.password("U-294b70ca67df4c018f59950f35314944")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.*;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import javax.annotation.PostConstruct;
import java.lang.invoke.MethodHandles;
import java.time.Duration;
import java.util.Objects;
import java.util.UUID;
import static org.springframework.security.config.Customizer.withDefaults;
#EnableWebSecurity
#Configuration(proxyBeanMethods = false)
public class TestAuthorizationServerConfig {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
#PostConstruct
public void publicNote() {
logger.info("****************************************************************************************************************************");
logger.info("****************************************************************************************************************************");
logger.info("Note that the current spring authorisation server does not support token renewal. If you want to test that, use the Keycloak");
logger.info("****************************************************************************************************************************");
logger.info("****************************************************************************************************************************");
}
public static CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addAllowedOrigin("127.0.0.1:4201");
config.addAllowedOrigin("http://127.0.0.1:4201");
config.addAllowedOrigin("localhost:4201");
config.addAllowedOrigin("127.0.0.1:4200");
config.addAllowedOrigin("http://127.0.0.1:4200");
config.addAllowedOrigin("localhost:4200");
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**", config);
return source;
}
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
return http.cors().configurationSource(corsConfigurationSource()).and()
.authorizeRequests()
.antMatchers("/**")
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/login")
.authenticated()
.and()
.formLogin(withDefaults())
.build();
}
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(withDefaults()).cors().configurationSource(corsConfigurationSource()).and().build();
}
#Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient publicClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("medical-share-openid-connect-client-id")
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("http://127.0.0.1:4200")
.redirectUri("http://127.0.0.1:4200/")
.redirectUri("http://127.0.0.1:4201")
.redirectUri("http://127.0.0.1:4201/")
.scope(OidcScopes.OPENID)
.tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofDays(1)).build())
.scope("offline_access")
// .scope("message.read")
// .scope("message.write")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).requireProofKey(true).build())
.build();
// Save registered client in db as if in-memory
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(publicClient);
return registeredClientRepository;
}
#Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
#Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
#Bean
public JWKSource<SecurityContext> jwkSource(OpenIdConnectMockController openIdConnectMockController) {
RSAKey rsaKey = openIdConnectMockController.getJwkRsaPublicKey();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
#Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder().issuer("http://127.0.0.1:8088").build();
}
#Bean
public EmbeddedDatabase embeddedDatabase() {
return new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.H2)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql")
.addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
}
/**
* This is a bit a hack, but as we do not know how we integrate the HealthPlattform this is a very easy way to solve the Problem for the moment.
*/
#Bean
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
return context -> {
Authentication principal = context.getPrincipal();
if (Objects.equals(context.getTokenType().getValue(), "access_token") && principal instanceof UsernamePasswordAuthenticationToken) {
User user = (User) principal.getPrincipal();
context.getClaims()
.claim("name", user.getUsername());
}
};
}
}
import {NgModule} from '#angular/core';
import {AuthModule, LogLevel} from 'angular-auth-oidc-client';
#NgModule({
imports: [
AuthModule.forRoot({
config: {
configId: 'ms-angular-auth-oidc-client-lib-id',
logLevel: LogLevel.Debug,
historyCleanupOff: false,
// Keycloak
// authority: 'http://localhost:8088/realms/master',
authority: 'http://127.0.0.1:8088',
redirectUrl: 'http://127.0.0.1:4201/',
// Keycloak
// redirectUrl: 'http://localhost:4201/',
postLogoutRedirectUri: 'http://127.0.0.1:4201/',
clientId: 'medical-share-openid-connect-client-id',
scope: 'openid',
responseType: 'code',
silentRenew: true,
renewTimeBeforeTokenExpiresInSeconds: 10,
autoUserInfo: false,
useRefreshToken: true,
secureRoutes: [
'http://127.0.0.1:4201',
'http://localhost:4201',
],
},
}),
],
exports: [AuthModule],
})
export class AuthConfigModule {
}

Related

Successful login response with a 404

Currently, I learning security and I'm trying to make a simple login for my Android app. I'm using a custom Filter to request access to my protected data. This way I don't need the default login-page, and use a custom login in my android app instead. But when I do a successful login, I get a 404 response instead of a 200. I there a way to change this as it is set up right now?
MAIN.class
package com.ssl.Oauth2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
Controller.class
#RestController
public class Controller {
#RequestMapping("/protected-resource")
public String protectedResource() {
return "The Protected Resource";
}
#RequestMapping("/public-resource")
public String publicResource() {
return "The Public Resource";
}
}
SecurityConfiguration.class
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("john").password("{noop}12345").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
CustomFilter mupaf = new CustomFilter();
mupaf.setAuthenticationManager(authenticationManager());
http
.csrf().disable()
.addFilterAt(
mupaf,
UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/protected-resource").authenticated()
.antMatchers(HttpMethod.POST, "/login").permitAll();
}
}
CustomFilter
package com.ssl.Oauth2;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
public class CustomFilter extends AbstractAuthenticationProcessingFilter {
protected CustomFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
//
}
#Override
public Authentication attemptAuthentication(
HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username, password;
// Credentials to Java Map
try {
Map<String, String> requestMap = new ObjectMapper().readValue(request.getInputStream(), Map.class);
username = requestMap.get("username");
password = requestMap.get("password");
} catch (IOException e) {
throw new AuthenticationServiceException(e.getMessage(), e);
}
// Token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// TEST
System.out.println("\n------------------------------------------------\n");
System.out.println("AUTHENTICATION REQUEST:\n" +this.getAuthenticationManager().authenticate(authRequest) );
System.out.println("\n------------------------------------------------\n");
// the method authenticate of the AuthenticationManager receives a token
return this.getAuthenticationManager().authenticate(authRequest);
}
}

Spring Authorization Server with AuthorizationGrantType.PASSWORD

I want to build an authorization server with Spring Authorization Server project. For now I want to use AuthorizationGrantType.PASSWORD.
I developed a demo project from the samples of Spring Authorization Server project. However, when I try to get token with the http://localhost:9000/oauth2/token?grant_type=password&username=user&password=pass (I'm using client_id and client_secret as basic auth in Postman) request, I'm getting 403.
What I'm missing here?
Dependencies: spring-boot-starter-web, spring-boot-starter-security, spring-security-oauth2-authorization-server (version: 0.2.2)
AuthorizationServerConfig class:
import java.security.*;
import java.security.interfaces.*;
import java.util.UUID;
import org.springframework.context.annotation.*;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.server.authorization.client.*;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
#Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
#Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("client1")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
#Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder()
.issuer("http://auth-server:9000")
.build();
}
#Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
private static RSAKey generateRsa() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
}
DefaultSecurityConfig class:
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
#EnableWebSecurity
public class DefaultSecurityConfig {
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors().disable()
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest()
.authenticated()
);
return http.build();
}
#Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("pass")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
}
There is this message in the log that I found It may be related:
o.s.s.w.a.i.FilterSecurityInterceptor : Failed to authorize filter invocation [POST /oauth2/token?grant_type=password&username=user&password=pass] with attributes [authenticated]
Looks like password grant_type is not supported. Probably that's why I was getting error.
source: https://github.com/spring-projects/spring-authorization-server/issues/126

Getting exception spring security User account is locked\

We have implemented Spring Security in our angular spring boot project.
Here we are getting exception spring security User account is locked
Please review the following code.
SecurityConfiguration.java
package com.jwt.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import com.jwt.security.filter.AuthenticationTokenFilter;
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration<jwtAuthenticationEntryPoint> extends WebSecurityConfigurerAdapter{
#Autowired private UserDetailsService userDetailsService;
#Autowired private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint ;
#Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(this.userDetailsService).passwordEncoder( PasswordEncoder());
}
#Bean
public PasswordEncoder PasswordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public AuthenticationTokenFilter authenticationTokenFilterBean( ) {
return new AuthenticationTokenFilter();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/**").permitAll()
.antMatchers("/registration").permitAll()
.antMatchers("/login").permitAll()
.antMatchers(HttpMethod.OPTIONS ,"/**").permitAll()
.anyRequest().authenticated();
httpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);
httpSecurity.headers().cacheControl();
httpSecurity.headers().httpStrictTransportSecurity().includeSubDomains(true).maxAgeInSeconds(31536000);
}
}
the authentication token filter AuthenticationTokenFilter.hjava
package com.jwt.security.filter;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import com.jwt.security.JwtTokenUtil;
public class AuthenticationTokenFilter extends OncePerRequestFilter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Value("${jwt.header}")
private String tokenHeader;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// TODO Auto-generated method stub
String authToken = request.getHeader(this.tokenHeader);
if (authToken != null && authToken.length() > 7) {
authToken = authToken.substring(7);
}
String username = jwtTokenUtil.getUsernameFromToken(authToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
boolean isValid = jwtTokenUtil.validateToken(authToken, userDetails);
if (isValid) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
filterChain.doFilter(request, response);
}
}
Here i am getting null for authToken when running from postman
The code for JwtUtil is as following
package com.jwt.security;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
#Component
public class JwtTokenUtil implements Serializable {
static final String CLAIM_KEY_USERNAME = "sub";
static final String CLAIM_KEY_AUDIENCE = "audience";
static final String CLAIM_KEY_CREATED = "created";
#Value("${jwt.secret}")
private String secret;
#Value("${jwt.expiration}")
private Long expiration;
public String getUsernameFromToken(String authToken) {
String username = null;
try {
final Claims claims = getClaimsFromToken(authToken);
username = claims.getSubject();
} catch (Exception e) {
// TODO Auto-generated catch block
username = null;
}
return username;
}
private Claims getClaimsFromToken(String authToken) {
// TODO Auto-generated method stub
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(authToken).getBody();
} catch (Exception e) {
// TODO Auto-generated catch block
claims = null;
}
return claims;
}
public boolean validateToken(String authToken, UserDetails userDetails) {
// TODO Auto-generated method stub
JwtUser user = (JwtUser) userDetails;
final String username = getUsernameFromToken(authToken);
return (username.equals(user.getUsername()) && !isTokenExpired(authToken));
}
private boolean isTokenExpired(String authToken) {
final Date expiration = getExpirationDateFromToken(authToken);
return expiration.before(new Date());
}
private Date getExpirationDateFromToken(String authToken) {
// TODO Auto-generated method stub
Date expiration = null;
final Claims claims = getClaimsFromToken(authToken);
if (claims != null) {
expiration = claims.getExpiration();
} else {
expiration = null;
}
return expiration;
}
public String generateToken(JwtUser userDetails) {
Map<String,Object> claims = new HashMap<String,Object>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
public String generateToken(Map<String , Object> claims ) {
return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate()).signWith(SignatureAlgorithm.HS512, secret).compact();
}
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
}
The code for CsrfHeaderFilter is as following
package com.jwt.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;
public class CsrfHeaderFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// TODO Auto-generated method stub
System.out.println("...CsrfToken.class.getName() :::" + CsrfToken.class.getName());
// CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
// CsrfToken csrfToken = new HttpSessionCsrfTokenRepository().loadToken(request);
CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
String token = null;
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
if(csrfToken != null) {
token = csrfToken.getToken();
}
if (cookie == null || token != null && !token.equals(cookie.getValue())) {
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
filterChain.doFilter(request, response);
}
}
The controller used is AuthenticationController The code is as following
package com.jwt.security.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.jwt.security.JwtTokenUtil;
import com.jwt.security.JwtUser;
import com.jwt.security.domain.User;
import com.jwt.security.domain.UserDTO;
import com.jwt.security.exception.UnauthorizedException;
#RestController
public class AuthenticationController {
#Value("${jwt.header}")
private String tokenHeader;
#Autowired private AuthenticationManager authenticationManager;
#Autowired private JwtTokenUtil jwtTokenUtil;
#PostMapping(value="/login")
public ResponseEntity<UserDTO> login(#RequestBody User user, HttpServletRequest request , HttpServletResponse response) {
try {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println("matches ::" + encoder.matches("123", user.getPassword()));
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword()));
final JwtUser userDetails = (JwtUser)authentication.getPrincipal();
SecurityContextHolder.getContext().setAuthentication(authentication);
final String token = jwtTokenUtil.generateToken(userDetails);
response.setHeader("Token", token);
return new ResponseEntity<UserDTO>(new UserDTO(userDetails.getUser(), token) , HttpStatus.OK);
}catch(UnauthorizedException ex) {
ex.printStackTrace();
throw new UnauthorizedException(ex.getMessage());
}
}
}
On calling http://localhost:8080/login from postman and passing the correct email and password , we are getting the following exception
org.springframework.security.authentication.LockedException: User account is locked
Please advice
The message says "User account is locked". This happens after a number of failed authentication events. The account eventually becomes unlocked depending on implementation.
There are only two places that happens in Spring Security:
AccountStatusUserDetailsChecker.check(UserDetails user)
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
throw new LockedException(messages.getMessage(
"AccountStatusUserDetailsChecker.locked", "User account is locked"));
}
if (!user.isEnabled()) {
throw new DisabledException(messages.getMessage(
"AccountStatusUserDetailsChecker.disabled", "User is disabled"));
}
if (!user.isAccountNonExpired()) {
throw new AccountExpiredException(
messages.getMessage("AccountStatusUserDetailsChecker.expired",
"User account has expired"));
}
if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(messages.getMessage(
"AccountStatusUserDetailsChecker.credentialsExpired",
"User credentials have expired"));
}
}
AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks.check(UserDetails user)
So if you want to set a breakpoint, that's where you start.
All of this happens in your UserDetailsService which you have in your configuration.
#Autowired private UserDetailsService userDetailsService;
This service returns an object that implements the UserDetails interface
public interface UserDetails {
boolean isAccountNonLocked();
}
if this method returns false, the account is locked. the name is a bit confusing.
Since we don't know what your UserDetailsService is, we can't tell you how this gets populated. So the recommendation is to just set a break point when the error is thrown.
If you don't want the account locking feature to be enabled, there are different ways to implement that. If you override the UserDetailsService bean you can always return users that are never locked.
Another way is to inject your own checker
DaoAuthenticationProvider daoProvider = ....
daoProvider.setPreAuthenticationChecks(toCheck -> {});
There is also a PostAuthenticationChecks object to see if your password has expired.

Spring Security OAuth2 SSO with Custom provider + logout

I'm trying to implement sso with Spring Security Oauth2 using Spring-boot and Dave Syer samples
I want to use my custom server provider and it's working fine.
For the client, I want user to be authenticate (so redirected to OAuth2 url) when they try to access client site (eg localhost:8080/) and redirect back to index.html file once authenticated. I also want to implement logout when user on a link in index.html file.
I've come up with this following client sso client:
package org.ikane;
import java.io.IOException;
import java.security.Principal;
import java.util.Arrays;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;
#SpringBootApplication
#Controller
public class DemoSsoOauth2ClientApplication implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(DemoSsoOauth2ClientApplication.class);
#Override
public void run(String... arg0) throws Exception {
SecurityContext securityContext = SecurityContextHolder.getContext();
try {
Authentication authentication = securityContext.getAuthentication();
logger.info(authentication.getDetails().toString());
SecurityContextHolder.clearContext();
} catch (Exception e) {
logger.error("Error", e);
}
}
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(DemoSsoOauth2ClientApplication.class, args);
ConfigurableEnvironment env = applicationContext.getEnvironment();
logger.info("\n\thttp://localhost:{}{}\n\tProfiles:{}\n",
StringUtils.defaultIfEmpty(env.getProperty("server.port"), "8080"),
StringUtils.defaultIfEmpty(env.getProperty("server.contextPath"), "/"),
Arrays.toString(env.getActiveProfiles()));
}
#RequestMapping(value="/")
public String home() {
return "index";
}
#RequestMapping(value="/user")
#ResponseBody
public Principal user(Principal user) {
return user;
}
/**
* The Class OAuthConfiguration that sets up the OAuth2 single sign on
* configuration and the web security associated with it.
*/
#Component
#Controller
#EnableOAuth2Sso
protected static class OAuthClientConfiguration extends WebSecurityConfigurerAdapter {
private static final String CSRF_COOKIE_NAME = "XSRF-TOKEN";
private static final String CSRF_ANGULAR_HEADER_NAME = "X-XSRF-TOKEN";
#Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests()
.antMatchers("/index.html", "/").permitAll().anyRequest()
.authenticated().and().csrf().csrfTokenRepository(csrfTokenRepository())
.and().addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
}
private Filter csrfHeaderFilter() {
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, CSRF_COOKIE_NAME);
String token = csrf.getToken();
if (cookie == null || token != null
&& !token.equals(cookie.getValue())) {
cookie = new Cookie(CSRF_COOKIE_NAME, token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
};
}
/**
* Angular sends the CSRF token in a custom header named "X-XSRF-TOKEN"
* rather than the default "X-CSRF-TOKEN" that Spring security expects.
* Hence we are now telling Spring security to expect the token in the
* "X-XSRF-TOKEN" header.
*
* This customization is added to the csrf() filter.
*
* #return
*/
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName(CSRF_ANGULAR_HEADER_NAME);
return repository;
}
}
}
You can find a GitHub source. Any hints on how to implement this use case?
Thanks in advance
To make your client app redirect to the Authorization Server just add
the annotation #EnableOAuth2Sso on your WebSecurityConfigurerAdapter and
place the proper OAuth2 configurations (client-id, secret, access token uri...) in your properties file.
(I'm assuming that your client app is using Spring Boot as well)
To end the user's session you have to redirect to an endpoint in the authorization server and logout programmatically as shown in this post.
I have created a repository on github with a sample app that has those features that you are looking for.
Please check it out and let me know if it helps you.

JedisDataException when upgrading spring-session from 1.0.0.RC1 to 1.0.0.RELEASE

We are trying to upgrade from RC1 to RELEASE, but we are now getting the following exception:
Caused by: redis.clients.jedis.exceptions.JedisDataException: ERR unknown command 'CONFIG'
at redis.clients.jedis.Protocol.processError(Protocol.java:100) ~[Protocol.class:na]
at redis.clients.jedis.Protocol.process(Protocol.java:118) ~[Protocol.class:na]
at redis.clients.jedis.Protocol.read(Protocol.java:187) ~[Protocol.class:na]
at redis.clients.jedis.Connection.getBinaryMultiBulkReply(Connection.java:212) ~[Connection.class:na]
at redis.clients.jedis.Connection.getMultiBulkReply(Connection.java:205) ~[Connection.class:na]
at redis.clients.jedis.Jedis.configGet(Jedis.java:2701) ~[Jedis.class:na]
at org.springframework.data.redis.connection.jedis.JedisConnection.getConfig(JedisConnection.java:531) ~[JedisConnection.class:1.3.0.RELEASE]
... 29 common frames omitted
Here is my SessionConfig:
#EnableRedisHttpSession
public class SessionConfig {
private static final int SESSION_EXPIRATION_TIME = 28800;
#Autowired
private Environment env;
#Bean
public JedisConnectionFactory connectionFactory() {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
jedisConnectionFactory.setHostName(env.getProperty("reddis.host"));
jedisConnectionFactory.setPort(env.getProperty("reddis.port", Integer.class));
return jedisConnectionFactory;
}
#Bean
public HttpSessionStrategy httpSessionStrategy() {
return new HeaderHttpSessionStrategy();
}
#Bean
public RedisOperationsSessionRepository sessionRepository() {
RedisOperationsSessionRepository redisOperationsSessionRepository = new RedisOperationsSessionRepository(connectionFactory());
redisOperationsSessionRepository.setDefaultMaxInactiveInterval(SESSION_EXPIRATION_TIME);
return redisOperationsSessionRepository;
}
}
We are using redis 2.8.6 with AWS ElastiCache, spring framework 4.1.4, spring-data-redis 1.3.0.RELEASE and jedis client 2.4.1.
Any idea why we are facing this issue?
Thanks!
You are experiencing this bug. In short, Spring Session is attempting to configure Redis for you and AWS disables configuring Redis (for security reasons).
To get around this, you can replace the EnableRedisHttpSession with an #Import(Issue124Config.class) and following configuration:
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.session.ExpiringSession;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.SessionMessageListener;
import org.springframework.session.web.http.HttpSessionStrategy;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.ClassUtils;
#Configuration
public class Issue124Config {
#Value("${spring.session.maxInactive ?: 1800}")
private Integer maxInactiveIntervalInSeconds;
private HttpSessionStrategy httpSessionStrategy;
#Bean
public RedisTemplate<String,ExpiringSession> sessionRedisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, ExpiringSession> template = new RedisTemplate<String, ExpiringSession>();
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(connectionFactory);
return template;
}
#Bean
public RedisOperationsSessionRepository sessionRepository(RedisTemplate<String, ExpiringSession> sessionRedisTemplate) {
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate);
sessionRepository.setDefaultMaxInactiveInterval(maxInactiveIntervalInSeconds);
return sessionRepository;
}
#Bean
public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(sessionRepository);
if(httpSessionStrategy != null) {
sessionRepositoryFilter.setHttpSessionStrategy(httpSessionStrategy);
}
return sessionRepositoryFilter;
}
#Autowired(required = false)
public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
this.httpSessionStrategy = httpSessionStrategy;
}
}

Resources