How I can enable logs to see actual http queries between my Spring Boot app verifying the tokens and the oauth2 server?
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: https://myhost/idm-services/auth/realms/myrealm
registration:
keycloak:
client-id: myclientid
logging:
level:
org:
springframework:
web:
filter:
CommonsRequestLoggingFilter: DEBUG
security:
oauth2: TRACE
I have this enabled, but I don't see any actual logs to oauth2.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated().and()
.oauth2Login().userInfoEndpoint().oidcUserService(this.oidcUserService());
}
#Bean
public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
OidcUser oidcUser = delegate.loadUser(userRequest);
log.debug("User "+oidcUser.toString());
log.debug("UserInfo "+oidcUser.getUserInfo().toString());
log.debug("UserClaims "+oidcUser.getClaims().toString());
log.debug("UserToken "+oidcUser.getIdToken().toString());
final Map<String, Object> claims = oidcUser.getClaims();
final JSONArray groups = (JSONArray) claims.get("org_access");
final Set<GrantedAuthority> mappedAuthorities = groups.stream()
.map(role -> new SimpleGrantedAuthority(""+role))
.collect(Collectors.toSet());
return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
};
}
there is the security configuration. I'd like to debug the token info transformation in oidcUserService, but the debug lines are not hit during the debug.
No way to trace in org.springframework.security.oauth2.server.resource.authentication
There are just no logging entries there. The only known stuff is to put all logging to trace, then you will see for example HttpConnection calls to ouath servers, that let you clarify what is going on. It is impossible to trace the tokens and data transmitted, so on the stage where you have no option to debug through the remote port, you have no option to understand what was the problem.
Related
I'm working on Spring Boot project based on microservices architecture on backend and Vue.js on frontend.
Structure of my project is next:
For avoiding CORS error usually I add #CrossOrigin annotation on to class and it works.
It was all good and has been working well, until I added security part with ability to login users.
What did I did:
1. To API Gateway that built on spring-cloud-gateway I've added AuthFilter that uses as interceptor to create and check JWT:
api-gateway/src/main/java/.../AuthFilter.java
#Component
public class AuthFilter extends AbstractGatewayFilterFactory<AuthFilter.Config> {
private final WebClient.Builder webClientBuilder;
#Autowired
public AuthFilter(WebClient.Builder webClientBuilder) {
super(Config.class);
this.webClientBuilder = webClientBuilder;
}
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
if(!exchange.getRequest().getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
throw new RuntimeException("Missing auth information");
}
String authHeader = exchange.getRequest().getHeaders().get(org.springframework.http.HttpHeaders.AUTHORIZATION).get(0);
String[] parts = authHeader.split(" ");
if(parts.length != 2 || !"Bearer".equals(parts[0])) {
throw new RuntimeException("Incorrect auth structure");
}
return webClientBuilder.build()
.post()
.uri("http://manager-service/api/v1/auth/validateToken?token=" + parts[1])
.retrieve()
.bodyToMono(EmployeeDTO.class) //EmployeeDTO.class is custom DTO that represents User
.map(user -> {
exchange.getRequest()
.mutate()
.header("x-auth-user-id", user.getId());
return exchange;
}).flatMap(chain::filter);
};
}
public static class Config {
//live it empty because we dont need any particular configuration
}
}
2. I've added AuthFilter as filter to each service in application.properties:
api-gateway/src/resource/application.properties
##Workshop service routes
spring.cloud.gateway.routes[0].id=workshop-service
spring.cloud.gateway.routes[0].uri=lb://workshop-service
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/v1/workshop/**
spring.cloud.gateway.routes[0].filters[0]=AuthFilter
##Manage service routes
spring.cloud.gateway.routes[1].id=manager-service
spring.cloud.gateway.routes[1].uri=lb://manager-service
spring.cloud.gateway.routes[1].predicates[0]=Path=/api/v1/manage/**
spring.cloud.gateway.routes[1].filters[0]=AuthFilter
##Manage service for singIn. Here we dont need to add AuthFilter, cause sign in page should be available for all
spring.cloud.gateway.routes[2].id=manager-service-sign-in
spring.cloud.gateway.routes[2].uri=lb://manager-service
spring.cloud.gateway.routes[2].predicates[0]=Path=/api/v1/auth/signIn
...
3. Manager-service microservice used to control base entities for system, such as users, roles, organizations where users working are and so on, so here I added SecurityConfig and WebConfig, because this microservice will be responsible for JWT generating:
manager-service/src/main/java/.../SecurityConfig.java
#EnableWebSecurity
public class SecurityConfig {
#Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.authorizeRequests().anyRequest().permitAll();
return httpSecurity.build();
}
}
manager-service/src/main/java/.../WebConfig.java
#EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
private static final Long MAX_AGE=3600L;
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedHeaders(
HttpHeaders.AUTHORIZATION,
HttpHeaders.CONTENT_TYPE,
HttpHeaders.ACCEPT)
.allowedMethods(
HttpMethod.GET.name(),
HttpMethod.POST.name(),
HttpMethod.PUT.name(),
HttpMethod.DELETE.name())
.maxAge(MAX_AGE)
.allowedOrigins("http://localhost:8100")
.allowCredentials(false);
}
}
4. In controller, that represents auth I also added #CrossOrigin annotation to class:
manager-service/src/main/java/.../AuthController.java
#RestController
#RequestMapping("api/v1/auth")
#CrossOrigin(origins = "http://localhost:8100")
#Slf4j
public class AuthController {
private final AuthService authService;
#Autowired
public AuthController(AuthService authService) {
this.authService = authService;
}
#PostMapping("/signIn")
public ResponseEntity<EmployeeDTO> signIn(#RequestBody CredentialsDTO credentialsDTO) {
log.info("Trying to login {}", credentialsDTO.getLogin());
return ResponseEntity.ok(EmployeeMapper.convertToDTO(authService.signIn(credentialsDTO)));
}
#PostMapping("/validateToken")
public ResponseEntity<EmployeeDTO> validateToken(#RequestParam String token) {
log.info("Trying to validate token {}", token);
Employee validatedTokenUser = authService.validateToken(token);
return ResponseEntity.ok(EmployeeMapper.convertToDTO(validatedTokenUser));
}
}
5. For frontend I use Vue.js. For requests I use axios. Here are post-request to login:
axios.post('http://localhost:8080/api/v1/auth/signIn', this.credentials).then(response => {
console.log('response = ', response)
console.log('token from response', response.data.token)
this.$store.commit('saveToken', response.data.token)
}).catch(error => {
console.log('Error is below')
console.log(error)
})
All what I'm getting is an error: Access to XMLHttpRequest at 'http://localhost:8080/api/v1/auth/signIn' from origin 'http://localhost:8100' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.. Below you'll see headers, that displays Chrome with request:
I've been trying to add another one corsConfiguration, tried to mark with CrossOrigin annotation only method, not class at all but it hadn't take any effects. If I try to make such requests with postman it gives me expected response with generated token.
I'll be grateful for any idea what could I do wrong.
Thanks!
UPDATE: As I understood well - all problems is in api-gateway. If I make requests directly to service - I get right response, but if I make request through gateway - I'm facing an error, logs of api-gateway below:
2022-07-05 00:34:18.128 TRACE 8105 --- [or-http-epoll-5] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "[/api/v1/workshop/**]" does not match against value "/api/v1/auth/signIn"
2022-07-05 00:34:18.129 TRACE 8105 --- [or-http-epoll-5] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "[/api/v1/manage/**]" does not match against value "/api/v1/auth/signIn"
2022-07-05 00:34:18.129 TRACE 8105 --- [or-http-epoll-5] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "/api/v1/auth/signIn" matches against value "/api/v1/auth/signIn"
2022-07-05 00:34:18.129 DEBUG 8105 --- [or-http-epoll-5] o.s.c.g.h.RoutePredicateHandlerMapping : Route matched: manager-service-sign-in
2022-07-05 00:34:18.129 DEBUG 8105 --- [or-http-epoll-5] o.s.c.g.h.RoutePredicateHandlerMapping : Mapping [Exchange: OPTIONS http://localhost:8080/api/v1/auth/signIn] to Route{id='manager-service-sign-in', uri=lb://manager-service, order=0, predicate=Paths: [/api/v1/auth/signIn], match trailing slash: true, gatewayFilters=[], metadata={}}
2022-07-05 00:34:18.129 DEBUG 8105 --- [or-http-epoll-5] o.s.c.g.h.RoutePredicateHandlerMapping : [e5b87280-8] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler#78df1cfc
After research I've solved problem. It was all Gateway's fault
As I mentioned before, direct request gives me right response, but only if I go through api-gateway it gives me an errors.
So solution is to add CORS Configuration rules to gateway:
spring:
cloud:
gateway:
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "http://localhost:8100"
allowedHeaders: "*"
allowedMethods: "*"
Please, note that if you don't add section with gateway: default-filters you will be facing similar error with header that contains multiple values.
Thanks to answer by Pablo Aragonés in another question and Spring Cloud Documentation
add this to applications.yml in your gateway, solved issue for me:
spring:
cloud:
gateway:
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-
Allow-Credentials, RETAIN_UNIQUE
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "http://localhost:<your port>"
allowedHeaders: "*"
allowedMethods: "*"
I have faced similar issue & got resolved by defining following bean in my Configuration.
#Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("http://localhost:8100")); // Provide list of origins if you want multiple origins
config.setAllowedHeaders(Arrays.asList("Origin", "Content-Type", "Accept"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH"));
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
You can comment out all other details e.g. #CrossOrigin(origins = "http://localhost:8100"), WebConfig.java, SecurityConfig.java as once we define above bean these things are not required.
Your code may not be running because you have defined bean , security config as well as webconfig which might be conflicting while processing your request.
I am using the swagger with springdoc-openapi-ui-1.4.3
#SecurityRequirement(name = "security_auth")
public class ProductController {}
Setting the security schema
#SecurityScheme(name = "security_auth", type = SecuritySchemeType.OAUTH2,
flows = #OAuthFlows(authorizationCode = #OAuthFlow(
authorizationUrl = "${springdoc.oAuthFlow.authorizationUrl}"
, tokenUrl = "${springdoc.oAuthFlow.tokenUrl}",scopes = {
#OAuthScope(name = "IdentityPortal.API", description = "IdentityPortal.API")})))
public class OpenApiConfig {}
Security config
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {// #formatter:off
http
.authorizeRequests()
.antMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html")
.permitAll()
.antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**")
.hasAuthority("SCOPE_read")
.antMatchers(HttpMethod.POST, "/api/foos")
.hasAuthority("SCOPE_write")
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer()
.jwt();
}
}
With dependencies
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springdoc:springdoc-openapi-ui:1.4.3'
implementation 'org.springdoc:springdoc-openapi-security:1.4.3'
implementation "org.springframework.boot:spring-boot-starter-security"
Config setting
spring:
profiles:
active: dev
####### resource server configuration properties
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://localhost:5001
jwk-set-uri: https://localhost:5001/connect/token
springdoc:
swagger-ui:
oauth:
clientId: Local
usepkcewithauthorizationcodegrant: true
oAuthFlow:
authorizationUrl: https://localhost:5001
tokenUrl: https://localhost:5001/connect/token
In the swagger UI, the clientId is empty and client secret is present, for authorization code + PKCE flow client secret should not present
Your property syntax
usepkcewithauthorizationcodegrant
is not correct:
Here is the right property for PKCE:
springdoc.swagger-ui.oauth.use-pkce-with-authorization-code-grant=true
To fill, the client id, just use:
springdoc.swagger-ui.oauth.client-id=yourSPAClientId
For your remark of the existing secret filed that can be hidden. This looks like an enhancement on the swagger-ui.
You should submit an enhancement on the swagger-ui project:
https://github.com/swagger-api/swagger-ui/
it is some time since you asked the question but I will respond for others information. The major issue is the misleading implementation of the UI. You are forced to use the authorization code flow in the configuration because the authorization code with PKCE is missing. So you must use the authorization code (because you need to provide authorization and token url) and place a dummy secret into the yaml. Example below.
#SecurityScheme(name = "security_auth", type = SecuritySchemeType.OAUTH2,
flows = #OAuthFlows(authorizationCode = #OAuthFlow(
authorizationUrl = "${springdoc.oAuthFlow.authorizationUrl}"
, tokenUrl = "${springdoc.oAuthFlow.tokenUrl}")))
public class OpenApiConfig {}
If you want to use PKCE instead of the pure implicit set proper attribute (as #brianbro pointed) and a dummy secret as:
springdoc.swagger-ui.oauth.use-pkce-with-authorization-code-grant=true
springdoc.swagger-ui.oauth.clent-secret=justFillerBecausePKCEisUsed
As last not least if you want to prefill the client_id use configuration:
springdoc.swagger-ui.oauth.client-id=YourClientId
I can not figure out how to use thoso two together. After user is authenticated, I have OAuth2AuthorizedClient in the context which then I can use like:
String clientRegistrationId = auth.getAuthorizedClientRegistrationId();
OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(clientRegistrationId, auth.getName());
accessToken = client.getAccessToken().getTokenValue();
So I can use accessToken in my application. But this is manual interceptor that I inject into RestTemplate. Also if I do this way than I have to handle all refreshToken logic with another interceptor. Here then comes OAuth2RestTemplate, it handles all accessToken, refresToken stuff which I don't need interceptors. But to use it I need OAuth2ProtectedResourceDetails:
public OAuth2RestTemplate(OAuth2ProtectedResourceDetails resource) {
this(resource, new DefaultOAuth2ClientContext());
}
which I don't if I use spring.security.oauth2.client.registration style. Because AuthorizationCodeResourceDetails created only if security.oauth2.client style is used.:
spring:
thymeleaf:
cache: false
security:
oauth2:
client:
registration:
example-authorization-code:
id: demo-webapp-oidc-code
client-id: demo-webapp-oidc-code
client-secret: secret
client-name: demo-webapp-oidc-code
provider: authServer
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
authorization-grant-type: authorization_code
client-authentication-method: post
scope: openid, address
provider:
authServer:
token-uri: http://localhost:8095/authServer/oidc/token
authorization-uri: http://localhost:8095/authServer/oidc/authorize
user-info-uri: http://localhost:8095/authServer/oidc/profile
jwk-set-uri: http://localhost:8095/authServer/oidc/jwks
user-name-attribute: id
This is how It accepts:
#Bean
#ConfigurationProperties(prefix = "security.oauth2.client")
#Primary
public AuthorizationCodeResourceDetails oauth2RemoteResource() {
return new AuthorizationCodeResourceDetails();
}
I can create it manually but than things getting complicated. Lets imagine somehow I created resourceDetails, than I create OAuth2RestTemplate with fllowing,
new OAuth2RestTemplate( resourceDetails, defaultOAuth2ClientContext);
which means I need to set new DefaultOAuth2ClientContext:
public DefaultOAuth2ClientContext(OAuth2AccessToken accessToken) {
this.accessToken = accessToken;
this.accessTokenRequest = new DefaultAccessTokenRequest();
}
which requires Oauth2AccessToken. But I have core.OAuth2AccessToken in my context ( OAuth2AuthorizedClient authorizedClient.getAccessToken), while it requires common.OAuth2AccessToken.
I've currently set up a Spring Cloud Gateway Reverse Proxy with the intention of:
a) Handling Authentication of multiple OAuth/OIDC providers, including obtaining a Token
b) Look up the details from the provider locally, ensuring that the OAuth user x Oauth provider combination are authorised
c) If authorised look up the Grants/Permissions, and forward the request to SCG, with a JWT containing details of the authorised principal.
d) If not authorised display a page displaying pertinent details from the OAuth2 Auth, and explain that they are not authorised.
I have achieved most steps, but I am having trouble incorporating step c) into Spring Security Webflux
What I want to do is take the OAuth2AuthenticationToken obtained from the Authentication exchange, perform the lookup in step, and return a
bespoke Prinicipal based on results.
This would then be used via code to either trigger the SCG behaviour, or display the page.
spring-boot.version>2.1.6.RELEASE
spring-cloud.version>Greenwich.SR2
My problem is I don't know the best way of doing this.
Use some hook in the OAuth2 client to perform the extra auth steps. I may need to return an OAuth2Principal in this case
Add an extra security filter into the chain after the authentication.
This would replace the OAuth2Principal with my own prncipal. I'm not sure whether it is legal to replace a Principal after it has been authenticated, possibly removing the authentication status
Writing a custom AuthN Provider that would proxy to the OAuth client, and once competed run it's own logic before signalling that it is authenticated. This seems a complicated approach, and I'm not sure what classes I would use for this.
I've read the Spring Security documentation, and understand the general architecture of Spring Security, but cannot work out the best way of solving this.
This is my spring security filter logic
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
#Profile("oauth")
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return addAuthZ(http)
.oauth2Login()
.and().build();
}
private ServerHttpSecurity addAuthZ(ServerHttpSecurity http) {
return http.authorizeExchange()
.anyExchange().authenticated().and();
}
}
And here is the config, I am using sample OAuth2 providers Google and Facebook and a custom OAuth2 provider provided using CAS
spring:
security:
oauth2:
client:
registration:
google:
client-id: SET_ME
client-secret: SET_ME
facebook:
client-id: SET_ME
client-secret: SET_ME
sgd-authn:
provider: sgd-authn
client-id: SET_ME
client-secret: SET_ME
scope: openid
client-authentication-method: secret
authorization-grant-type: authorization_code
#redirect-uri: "{baseUrl}/oauth2/
redirect-uri-template: "{baseUrl}/{action}/oauth2/code/{registrationId}"
provider:
# These are needed for talking to CAS OIDC
sgd-authn:
authorization-uri: ${cas.url}/oidc/authorize
token-uri: ${cas.url}/oidc/accessToken
jwk-set-uri: ${cas.url}/oidc/jwks
user-info-uri: ${cas.url}/oidc/profile
user-name-attribute: sub
Well I managed to get something working using option 2, adding a filter after authentication.
Write a filter that takes in an Authentication (OAuth), runs it through some logic, and returns a new Authentication object that is instanceof a specific superclass
Arrange for this to return isAuthenticated = false if we could not look up the OAuth2 details.
Write a bespoke handler for NotAuthenticated
The filter is registered using something like this:
#Bean
public SecurityWebFilterChain springSecurityFilterChain(
ServerHttpSecurity http,
WebFilter proxyAuthFilter
) {
return http.addFilterAt(proxyAuthFilter, SecurityWebFiltersOrder.AUTHENTICATION); // Do configuration ...
}
and the filter is something like this:
public class ProxyAuthFilter implements WebFilter {
static private Logger LOGGER = LoggerFactory.getLogger(ProxyAuthFilter.class);
private final AuthZClientReactive authZClient;
public ProxyAuthFilter(AuthZClientReactive authZClient) {
this.authZClient = authZClient;
}
/**
* Process the Web request and (optionally) delegate to the next
* {#code WebFilter} through the given {#link WebFilterChain}.
*
* #param exchange the current server exchange
* #param chain provides a way to delegate to the next filter
* #return {#code Mono<Void>} to indicate when request processing is complete
*/
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return ReactiveSecurityContextHolder.getContext()
.flatMap(this::transform)
.then(chain.filter(exchange));
}
private Mono<Authentication> transform(SecurityContext securityContext) {
Authentication authentication = securityContext.getAuthentication();
if (authentication.isAuthenticated()) {
return authenticate(authentication)
.map(a -> {
securityContext.setAuthentication(a);
return a;
});
} else {
LOGGER.info("ProxyFilter - not authenticated {}", authentication);
return Mono.just(authentication);
}
}
// Runs the chain, then returns a Mono with the exchange object which completes
// when the auth header is added to the request.
private Mono<Authentication> authenticate(Authentication authentication) {
LOGGER.info("ProxyFilter - Checking authorisation for {}", authentication);
Mono<AuthTokenInfo.Builder> authInfo = null;
// Catch if this filter runs twice
if (authentication instanceof ProxyAuthentication) {
LOGGER.info("ProxyAuthentication already found");
return Mono.just(authentication);
} else if (authentication instanceof UsernamePasswordAuthenticationToken) {
// If lookup is successful, returns instance of ProxyAuthentication
return getUsernamePasswordAuth((UsernamePasswordAuthenticationToken) authentication);
} else if (authentication instanceof OAuth2AuthenticationToken) {
// If lookup is successful, returns instance of ProxyAuthentication
return getOAuthAuth((OAuth2AuthenticationToken) authentication);
} else {
LOGGER.info("Unknown principal {}", authentication);
// Signals a failed authentication, can be picked up by error page
// to display bespoke information
return Mono.just(new ProxyAuthenticationNotAuthenticated(
authentication, ProxyAuthenticationNotAuthenticated.Reason.UnknownAuthenticationType)
);
}
}
According the tutorial Spring Boot and OAuth2
I have following project structure:
And following source code:
SocialApplication.class:
#SpringBootApplication
#RestController
#EnableOAuth2Client
#EnableAuthorizationServer
#Order(200)
public class SocialApplication extends WebSecurityConfigurerAdapter {
#Autowired
OAuth2ClientContext oauth2ClientContext;
#RequestMapping({ "/user", "/me" })
public Map<String, String> user(Principal principal) {
Map<String, String> map = new LinkedHashMap<>();
map.put("name", principal.getName());
return map;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.antMatcher("/**").authorizeRequests().antMatchers("/", "/login**", "/webjars/**").permitAll().anyRequest()
.authenticated().and().exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/")).and().logout()
.logoutSuccessUrl("/").permitAll().and().csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
// #formatter:on
}
#Configuration
#EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.antMatcher("/me").authorizeRequests().anyRequest().authenticated();
// #formatter:on
}
}
public static void main(String[] args) {
SpringApplication.run(SocialApplication.class, args);
}
#Bean
public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<OAuth2ClientContextFilter>();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
#Bean
#ConfigurationProperties("github")
public ClientResources github() {
return new ClientResources();
}
#Bean
#ConfigurationProperties("facebook")
public ClientResources facebook() {
return new ClientResources();
}
private Filter ssoFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
filters.add(ssoFilter(facebook(), "/login/facebook"));
filters.add(ssoFilter(github(), "/login/github"));
filter.setFilters(filters);
return filter;
}
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
path);
OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
filter.setRestTemplate(template);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(
client.getResource().getUserInfoUri(),
client.getClient().getClientId());
tokenServices.setRestTemplate(template);
filter.setTokenServices(new UserInfoTokenServices(
client.getResource().getUserInfoUri(),
client.getClient().getClientId()));
return filter;
}
}
class ClientResources {
#NestedConfigurationProperty
private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();
#NestedConfigurationProperty
private ResourceServerProperties resource = new ResourceServerProperties();
public AuthorizationCodeResourceDetails getClient() {
return client;
}
public ResourceServerProperties getResource() {
return resource;
}
}
index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>Demo</title>
<meta name="description" content=""/>
<meta name="viewport" content="width=device-width"/>
<base href="/"/>
<link rel="stylesheet" type="text/css"
href="/webjars/bootstrap/css/bootstrap.min.css"/>
<script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script>
<script type="text/javascript"
src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<h1>Login</h1>
<div class="container unauthenticated">
With Facebook: click here
</div>
<div class="container authenticated" style="display: none">
Logged in as: <span id="user"></span>
<div>
<button onClick="logout()" class="btn btn-primary">Logout</button>
</div>
</div>
<script type="text/javascript"
src="/webjars/js-cookie/js.cookie.js"></script>
<script type="text/javascript">
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (settings.type == 'POST' || settings.type == 'PUT'
|| settings.type == 'DELETE') {
if (!(/^http:.*/.test(settings.url) || /^https:.*/
.test(settings.url))) {
// Only send the token to relative URLs i.e. locally.
xhr.setRequestHeader("X-XSRF-TOKEN",
Cookies.get('XSRF-TOKEN'));
}
}
}
});
$.get("/user", function (data) {
$("#user").html(data.userAuthentication.details.name);
$(".unauthenticated").hide();
$(".authenticated").show();
});
var logout = function () {
$.post("/logout", function () {
$("#user").html('');
$(".unauthenticated").show();
$(".authenticated").hide();
});
return true;
}
</script>
</body>
</html>
application.yml:
server:
port: 8080
security:
oauth2:
client:
client-id: acme
client-secret: acmesecret
scope: read,write
auto-approve-scopes: '.*'
facebook:
client:
clientId: 233668646673605
clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
accessTokenUri: https://graph.facebook.com/oauth/access_token
userAuthorizationUri: https://www.facebook.com/dialog/oauth
tokenName: oauth_token
authenticationScheme: query
clientAuthenticationScheme: form
resource:
userInfoUri: https://graph.facebook.com/me
github:
client:
clientId: bd1c0a783ccdd1c9b9e4
clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
accessTokenUri: https://github.com/login/oauth/access_token
userAuthorizationUri: https://github.com/login/oauth/authorize
clientAuthenticationScheme: form
resource:
userInfoUri: https://api.github.com/user
logging:
level:
org.springframework.security: DEBUG
But when I open browser and try to hit http://localhost:8080
In browser console I see:
(index):44 Uncaught TypeError: Cannot read property 'details' of undefined
at Object.success ((index):44)
at j (jquery.js:3073)
at Object.fireWith [as resolveWith] (jquery.js:3185)
at x (jquery.js:8251)
at XMLHttpRequest.<anonymous> (jquery.js:8598)
in code:
$.get("/user", function (data) {
$("#user").html(data.userAuthentication.details.name);
$(".unauthenticated").hide();
$(".authenticated").show();
});
It happens because /user response with 302 status code and js callback try to parse result of localhost:8080:
I don't understand why this redirect happens. Can you explain this behavior and help to fix it?
UPDATE
I took this code from https://github.com/spring-guides/tut-spring-boot-oauth2
important:
It reproduces only after I start client application.
P.S.
How to reproduce:
To test the new features you can just run both apps and visit
localhost:9999/client in your browser. The client app will redirect to
the local Authorization Server, which then gives the user the usual
choice of authentication with Facebook or Github. Once that is
complete control returns to the test client, the local access token is
granted and authentication is complete (you should see a "Hello"
message in your browser). If you are already authenticated with Github
or Facebook you may not even notice the remote authentication
ANSWER:
https://stackoverflow.com/a/50349078/2674303
Update: 15-May-2018
As you have already found out the solution, the issue happens because of the JSESSIONID gets overwritten
Update: 10-May-2018
Well your persistence with the 3rd bounty has finally paid off. I started digging into what was different between the two examples you had in the repo
If you look at the manual repo and /user mapping
#RequestMapping("/user")
public Principal user(Principal principal) {
return principal;
}
As you can see you are returning the principal here, you get more details from the same object. Now in your code that you run from auth-server folder
#RequestMapping({ "/user", "/me" })
public Map<String, String> user(Principal principal) {
Map<String, String> map = new LinkedHashMap<>();
map.put("name", principal.getName());
return map;
}
As you can see you only returned the name in the /user mapping and your UI logic runs below
$.get("/user", function(data) {
$("#user").html(data.userAuthentication.details.name);
$(".unauthenticated").hide();
$(".authenticated").show();
});
So the json response returned from /user api expected to have userAuthentication.details.name by the UI doesn't have that details. Now if I updated the method like below in the same project
#RequestMapping({"/user", "/me"})
public Map<String, Object> user(Principal principal) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("name", principal.getName());
OAuth2Authentication user = (OAuth2Authentication) principal;
map.put("userAuthentication", new HashMap<String, Object>(){{
put("details", user.getUserAuthentication().getDetails());
}});
return map;
}
And then check the application, it works
Original Answer
So the issue that you are running wrong project from the repo. The project you are running is auth-server which is for launching your own oauth server. The project that you need to run is inside manual folder.
Now if you look at the below code
OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter(
"/login/facebook");
OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
facebookFilter.setRestTemplate(facebookTemplate);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(),
facebook().getClientId());
tokenServices.setRestTemplate(facebookTemplate);
facebookFilter.setTokenServices(
new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId()));
return facebookFilter;
And the actual code you run has
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
path);
OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
filter.setRestTemplate(template);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(
client.getResource().getUserInfoUri(), client.getClient().getClientId());
tokenServices.setRestTemplate(template);
filter.setTokenServices(tokenServices);
return filter;
}
In your current the userdetails from the facebook doesn't get collected. That's is why you see an error
Because when you logged in the user, you didn't collect its user details. So when you access the details, its not there. And hence you get an error
If you run the correct manual folder, it works
I see two queries in your post.
ONE-
(index):44 Uncaught TypeError: Cannot read property 'details' of undefined
This was happening because you were perhaps running a wrong project (i.e. auth-server) which has a bug. The repo contains other similar projects also without bug. If you run the project manual or github this error will not appear. In these projects the javascript code is correctly handling the data which is returned by the server after authentication.
TWO-
/user response with 302 status code:
To understanding why this is happening lets see the security configuration of this application.
The end points "/", "/login**" and "/logout" are accessible to all.
All the other end points including "/user" requires authentication because you have used
.anyRequest().authenticated().and().exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/"))
So any request that is not authenticated will be redirected to the authentication entry point i.e. "/", asking user for authentication. It does not depend on whether your client application is started or not. As long as the request is not authenticated it will be redirected to "/". This why the spring controller is responding with status 302. Once you have authenticated with either of facebook or github, subsequent requests to "/user" end point will respond success with 200.
AND NEXT-
The endpoint "/me" in your application is secured as a secured resource with #EnableResourceServer. Since ResourceServerConfiguration has higher precedence (ordered 3 by default) than WebSecurityConfigurerAdapter(default 100, anyway it has already ordered explicitly lower than 3 with #Order annotation in the code) so ResourceServerConfiguration will apply for this endpoint. That means if the request is not authenticated then it will not be redirected to authentication entry point, it will rather return a response 401. Once you are authenticated it will response success with 200.
Hope this will clarify all your questions.
UPDATE- to answer your question
The repository link that you have provided in your post contains many projects. The projects auth-server, manual and github are all similar (provide the same functionality i.e. authentication with facebook and github). Just the index.html in auth-server projet has one bug. If you correct this bug that is replace
$("#user").html(data.userAuthentication.details.name);
with
$("#user").html(data.name);
it will also run fine. All the three projects will give the same output.
Finally I've found the problem. I see this behaviour due the fact of cookies clashing for client and server if you start both applications on localhost.
It happens due the fact of usage wrong property for context.
So to fix application you need to replace:
server:
context-path: /client
with
server:
servlet:
context-path: /client
P.S.
I've created issue on github:
https://github.com/spring-guides/tut-spring-boot-oauth2/issues/80
and made the pull request:
https://github.com/spring-guides/tut-spring-boot-oauth2/pull/81
P.S.2
Finally my pull request was merged:
https://github.com/spring-guides/tut-spring-boot-oauth2/pull/81