Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Official Spring security oauth2 example doesn't work because of cookies clashing(authorization code mechanism)

According the tutorial Spring Boot and OAuth2

I have following project structure:

enter image description here

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: <a href="/login/facebook">click here</a>
</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:

enter image description here

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

like image 449
gstackoverflow Avatar asked Apr 20 '18 13:04

gstackoverflow


People also ask

Is Spring Security OAuth2 Autoconfigure deprecated?

Spring Security OAuth2 project is currently deprecated and Spring Security team has decided to no longer provide support for authorization servers.

What is OAuth 2.0 and how it works in spring boot?

OAuth2 is an authorization framework that enables the application Web Security to access the resources from the client. To build an OAuth2 application, we need to focus on the Grant Type (Authorization code), Client ID and Client secret.


3 Answers

Update: 15-May-2018

As you have already found out the solution, the issue happens because of the JSESSIONID gets overwritten

Session ID replaced

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

OAuth Success

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

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

Working

like image 126
Tarun Lalwani Avatar answered Oct 22 '22 12:10

Tarun Lalwani


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.

like image 23
vsoni Avatar answered Oct 22 '22 13:10

vsoni


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

like image 5
gstackoverflow Avatar answered Oct 22 '22 12:10

gstackoverflow