spring-guides / tut-spring-security-and-angular-js

Spring Security and Angular:: A tutorial on how to use Spring Security with a single page application with various backend architectures, ranging from a simple single server to an API gateway with OAuth2 authentication.

Home Page:https://spring.io/guides/tutorials/spring-security-and-angular-js/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

oauth2 + spring session + failover

bilak opened this issue · comments

I'm trying to follow your double example. I'm trying to create sample where I want to have users authenticated/authorized with oauth2 and if possible session persisted in jdbc (to support basic failover in replicated application).
Instead of working with cookie, I'd like to work with http headers only. My current issue is that when I get to gateway at localhost:8080/login, what will forward me to uaa service at localhost:8080/uaa/login will generate another csrf token and after successful login it complains about the token found in header which is basically the token from gateway.

  1. Is it somehow possible to "share" csrf between application contexts? I'm thinking about spring session for this.
  2. Can you please share some solution to have oauth2 + spring session working?

Many thanks

I don't think there is a JDBC implementation of Spring Session (I might be wrong I guess). If there was it would be pretty routine to install it. Why not get it working with Redis first? All you need to do is declare the dependencies for vanilla use cases.

There already is EnableJdbcHttpSession and that's not problem to configure. But at first I just want to know if there is some possibility to resolve my csrf issue without disabling csrf.
Thanks

I don't understand the question then. If you have spring session set up correctly then everything you need is there in the session.

@dsyer problem is, that if I use the header HeaderHttpSessionStrategy and click on one of those two link which are navigating to /ui or /admin I'm asked for credentials (basic auth), because there is only header information (x-auth-token) and no cookie. So here is the question how to pass headers or if it's even possible while using only header strategy.

Why do you want to use the header strategy? A UI with a browser client (like an auth server) is better with the default cookie strategy, I would have thought. I feel like maybe I'm missing something.

I'm thinking about header strategy, because in the future I'd like to have some /api accessible for external services (not implemented by me). So for this I just want to create auth with header strategy. But maybe I'm thinking about it in bad way and it could be achieved simpler. Maybe I can add header in header filter to have both of them (cookie + header).

@dsyer I have now following application. It's based on your double and authorization server from oauth2. Instead of using JWT I've configured auth server to use inmemory store and I'm using redis to store sessions in backend store. Now when I access url localhost:8080 I'm redirected to localhost:9999/uaa/login what is ok. Then I log in, approve the ticket and after that, I'm navigated back to localhost:8080 but instead of index.html I'm getting localhost:8080/login?error=access_denied&error_description=User%20denied%20access&state=lJUt7S. Please can you tell me what am I missing here?
Thank you

From error_description=User denied access I would say that maybe the user denied access in the auth server?

@dsyer thanks...I've added GlobalAuthenticationConfigurerAdapter to have two users. Now after token approval I have following Authentication request failed: org.springframework.security.authentication.BadCredentialsException: Could not obtain access token. Has it something to do with token approval?
Edit:
Just to mention exception is now Possible CSRF detected - state parameter was required but no state could be found. I'm currently using in every application annotation @EnableResourceServer. Do I need it for each application or is it better to use '@EnableOAuth2Client'? I don't follow the differences right now.

@dsyer can you please help me to resolve I hope last issue (application is updated an seams to be working)? Currently when I authenticate on uaa server I'm navigated back to gateway at localhost:8080/login. Problem is that gateway doesn't know about session as can be viewd in 2.nd step in securityfilterchain (SecurityContextPersistenceFilter) .
When I access url localhost:8080 after prvious steps, I'm able to access to UI and work further.
I think I must somehow tell the gateway to use the session which was created during authentication at uaa. But is it right decision or should I do something completely different?

Thanks

o.s.s.web.DefaultRedirectStrategy        : Redirecting to 'http://localhost:9999/uaa/oauth/authorize?client_id=acme&redirect_uri=http://localhost:8080/login&response_type=code&state=x7GTpX'
o.s.s.web.util.matcher.OrRequestMatcher  : Trying to match using Ant [pattern='/css/**']
o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/login'; against '/css/**'
o.s.s.web.util.matcher.OrRequestMatcher  : Trying to match using Ant [pattern='/js/**']
o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/login'; against '/js/**'
o.s.s.web.util.matcher.OrRequestMatcher  : Trying to match using Ant [pattern='/images/**']
o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/login'; against '/images/**'
o.s.s.web.util.matcher.OrRequestMatcher  : Trying to match using Ant [pattern='/**/favicon.ico']
o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/login'; against '/**/favicon.ico'
o.s.s.web.util.matcher.OrRequestMatcher  : Trying to match using Ant [pattern='/error']
o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/login'; against '/error'
o.s.s.web.util.matcher.OrRequestMatcher  : No matches found
o.s.s.w.u.matcher.AntPathRequestMatcher  : Request '/login' matched by universal pattern '/**'
o.s.security.web.FilterChainProxy        : /login?code=nYd9AN&state=x7GTpX at position 1 of 12 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
o.s.security.web.FilterChainProxy        : /login?code=nYd9AN&state=x7GTpX at position 2 of 12 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
w.c.HttpSessionSecurityContextRepository : No SecurityContext was available from the HttpSession: null. A new one will be created.
o.s.security.web.FilterChainProxy        : /login?code=nYd9AN&state=x7GTpX at position 3 of 12 in additional filter chain; firing Filter: 'HeaderWriterFilter'
o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@23658495
o.s.security.web.FilterChainProxy        : /login?code=nYd9AN&state=x7GTpX at position 4 of 12 in additional filter chain; firing Filter: 'CsrfFilter'
o.s.security.web.FilterChainProxy        : /login?code=nYd9AN&state=x7GTpX at position 5 of 12 in additional filter chain; firing Filter: 'LogoutFilter'
o.s.s.w.u.matcher.AntPathRequestMatcher  : Request 'GET /login' doesn't match 'POST /logout
o.s.security.web.FilterChainProxy        : /login?code=nYd9AN&state=x7GTpX at position 6 of 12 in additional filter chain; firing Filter: 'OAuth2ClientAuthenticationProcessingFilter'
o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/login'; against '/login'
uth2ClientAuthenticationProcessingFilter : Request is to process authentication
uth2ClientAuthenticationProcessingFilter : Authentication request failed: org.springframework.security.authentication.BadCredentialsException: Could not obtain access token
uth2ClientAuthenticationProcessingFilter : Updated SecurityContextHolder to contain null Authentication
uth2ClientAuthenticationProcessingFilter : Delegating to authentication failure handler org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler@2536a681
.a.SimpleUrlAuthenticationFailureHandler : No failure URL set, sending 401 Unauthorized error
w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed

I'm not sure I follow that, but it's normal to have 2 sessions (one at the auth server and one at the client app). So I don't think there's an issue with the sessions or cookies. Your logs say "Could not obtain access token". So that would be an issue (probably a config error).

@dsyer it debug it looks like the AccessTokenRequest.preservedState is null hovewer state is provided in requests /login?code=nYd9AN&state=x7GTpX. Am I missing something?

Do you actually have a session on the client app? If not that could be the problem.

@dsyer I'm sharing the session using spring-session (persited in redis) so it should be there. But while I have @EnableOAuth2Sso on gateway I think it won't create the session on the gateway application (in this case the client) - I think I'm navigate dirrectly to uaa. Should the @EnableResourceServer help here or is there any pattern which to follow?

@EnableOAuth2Sso doesn't prevent a session from being created. Look at your requests and responses in the browser and try to see where the cookies get set and unset.

@dsyer you are right, cookies are created. But I don't understand what's going on in that last 3 steps

GET / HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: sk,cs-CZ;q=0.8,cs;q=0.6

HTTP/1.1 302 Found
Server: Apache-Coyote/1.1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: SESSION=7af6f797-19a4-41f5-9d3f-8d0a935f6d9e; Path=/; HttpOnly
Location: http://localhost:8080/login
Content-Length: 0
Date: Tue, 07 Jun 2016 13:56:21 GMT
GET /login HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: sk,cs-CZ;q=0.8,cs;q=0.6
Cookie: SESSION=7af6f797-19a4-41f5-9d3f-8d0a935f6d9e

HTTP/1.1 302 Found
Server: Apache-Coyote/1.1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Location: http://localhost:9999/uaa/oauth/authorize?client_id=acme&redirect_uri=http://localhost:8080/login&response_type=code&state=C1ZFiz
Content-Length: 0
Date: Tue, 07 Jun 2016 13:56:21 GMT
GET /uaa/oauth/authorize?client_id=acme&redirect_uri=http://localhost:8080/login&response_type=code&state=C1ZFiz HTTP/1.1
Host: localhost:9999
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: sk,cs-CZ;q=0.8,cs;q=0.6
Cookie: SESSION=7af6f797-19a4-41f5-9d3f-8d0a935f6d9e

HTTP/1.1 302 Found
Server: Apache-Coyote/1.1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Location: http://localhost:9999/uaa/login
Content-Length: 0
Date: Tue, 07 Jun 2016 13:56:21 GMT
GET /uaa/login HTTP/1.1
Host: localhost:9999
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: sk,cs-CZ;q=0.8,cs;q=0.6
Cookie: SESSION=7af6f797-19a4-41f5-9d3f-8d0a935f6d9e

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
X-Application-Context: application:9999
Content-Type: text/html;charset=UTF-8
Content-Language: sk
Transfer-Encoding: chunked
Date: Tue, 07 Jun 2016 13:56:21 GMT
POST /uaa/login HTTP/1.1
Host: localhost:9999
Connection: keep-alive
Content-Length: 72
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: http://localhost:9999
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Referer: http://localhost:9999/uaa/login
Accept-Encoding: gzip, deflate
Accept-Language: sk,cs-CZ;q=0.8,cs;q=0.6
Cookie: SESSION=7af6f797-19a4-41f5-9d3f-8d0a935f6d9e

HTTP/1.1 302 Found
Server: Apache-Coyote/1.1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: SESSION=7712e5ee-583d-4503-a6c4-349ced3d1b79; Path=/uaa/; HttpOnly
Location: http://localhost:9999/uaa/oauth/authorize?client_id=acme&redirect_uri=http://localhost:8080/login&response_type=code&state=C1ZFiz
Content-Length: 0
Date: Tue, 07 Jun 2016 13:56:40 GMT
GET /uaa/oauth/authorize?client_id=acme&redirect_uri=http://localhost:8080/login&response_type=code&state=C1ZFiz HTTP/1.1
Host: localhost:9999
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
Referer: http://localhost:9999/uaa/login
Accept-Encoding: gzip, deflate, sdch
Accept-Language: sk,cs-CZ;q=0.8,cs;q=0.6
Cookie: SESSION=7712e5ee-583d-4503-a6c4-349ced3d1b79; SESSION=7af6f797-19a4-41f5-9d3f-8d0a935f6d9e

HTTP/1.1 302 Found
Server: Apache-Coyote/1.1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
X-Application-Context: application:9999
Location: http://localhost:8080/login?code=NntBz1&state=C1ZFiz
Content-Language: sk
Content-Length: 0
Date: Tue, 07 Jun 2016 13:56:40 GMT
GET /login?code=NntBz1&state=C1ZFiz HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
Referer: http://localhost:9999/uaa/login
Accept-Encoding: gzip, deflate, sdch
Accept-Language: sk,cs-CZ;q=0.8,cs;q=0.6
Cookie: SESSION=7af6f797-19a4-41f5-9d3f-8d0a935f6d9e

HTTP/1.1 401 Unauthorized
Server: Apache-Coyote/1.1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: SESSION=ea63d65e-84d2-49b9-9afe-6d13c098a3a9; Path=/; HttpOnly
Content-Type: text/html;charset=UTF-8
Content-Language: sk
Content-Length: 342
Date: Tue, 07 Jun 2016 13:56:40 GMT

That all looks good to me (except the 401 of course). It seems like you have screwed up the client configuration somehow. What makes you think the problem is with the state in the session? Look at DEBUG logs on the auth server to find out why the access token is not granted?

@dsyer I've debuged the code and getting this Possible CSRF detected - state parameter was required but no state could be found and in AuthorizationCodeAccessTokenProvider.getParametersForTokenRequest I can see that request.getPreservedState() is returning the null.

This is my client configuration

security:
  sessions: ALWAYS
  oauth2:
    client:
      accessTokenUri: http://localhost:9999/uaa/oauth/token
      userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize
      clientId: acme
      clientSecret: acmesecret
      #registered-redirect-uri: http://localhost:8080
      #use-current-uri: false
      #pre-established-redirect-uri: http://localhost:8080
    resource:
      userInfoUri: http://localhost:9999/uaa/user
  basic:
    enabled: false

and this is uaa config

@Configuration
    @EnableAuthorizationServer
    protected static class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {

        @Autowired
        private AuthenticationManager authenticationManager;

        @Bean
        protected AuthorizationCodeServices authorizationCodeServices() {
            return new InMemoryAuthorizationCodeServices();
        }

        @Bean
        AccessTokenConverter accessTokenConverter() {
            return new DefaultAccessTokenConverter();
        }

        @Bean
        TokenStore tokenStore() {
            return new InMemoryTokenStore();
        }

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                    .withClient("acme")
                    .secret("acmesecret")
                    .authorizedGrantTypes("authorization_code", "refresh_token", "password")
                    .autoApprove(".*") // uncomment this to disable approval page
                    .scopes("openid");
                    //.redirectUris("http://localhost:8080");
        }

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints)
                throws Exception {
            endpoints
                    .authorizationCodeServices(authorizationCodeServices())
                    .accessTokenConverter(accessTokenConverter())
                    .authenticationManager(authenticationManager)
                    .tokenStore(tokenStore())
                    .approvalStoreDisabled();
        }

        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
            oauthServer
                    .tokenKeyAccess("permitAll()")
                    .checkTokenAccess("isAuthenticated()");
        }

    }

Does it work without Spring Session?

@dsyer yes it works without Spring Session. Do you have some hint what could be wrong here?

I think the sessions might be colliding in Redis. Your browser is sending 2 cookies to the auth server, and I would only expect one, for instance. How about if you configure the auth server to store sessions in a different database or something?

@dsyer I've disabled the spring-session on uaa and it's working. I'm wondering if the session could be shared from within uaa. I don't understand how those sessions are now working. Please correct me in following steps:

  1. I'm accessing gateway at localhost:8080 where the session=1 is created
  2. I'm navigated by security to localhost:8080/login session=1
  3. While EnableOauthSso is enabled I'm redirected to uaa at localhost:9999/uaa/login where session=1 and new jsessionid=2 is created
  4. When correct credentials are added and clicked on login I'm redirected back to localhost:8080/login with session=1
  5. While session=1 is not authenticated new session=3 is created and this is the session which I should use when I want to have HA

Is it ok from security perspective to have uaa with it's own session?

Many thanks

Is it ok from security perspective to have uaa with it's own session?

That's normal. I don't know why you would have any concerns about that.

I think this is probably a bug in Spring Session (2 cookies with different paths shouldn't collide like that). Maybe you could create a new ticket there?

@dsyer I've created spring-projects/spring-session#543.
Now when I look into redis everytime when I access the context /admin or /ui new sessions are created. Is it possible to don't create new sessions and somehow reuse those existing? I've already configured spring.sessions=never to test this behaviour in admin and ui applications, but sessions are still created.

I don't think you should be sharing sessions between the app and the auth server. That's what is causing all the problems.

N.B. spring.sessions=never is not a valid config setting from Spring Boot AFAIK, so I wouldn't expect it to do anything. But security.sessions=never would be a bad idea - you need sessions in both places.

@dsyer sorry that was my typo I used security.sessions=never.
About that sharing - I mean share sessions between the UI applications. Not auth server.
And more specificially...
If I create session for admin application and then navigate to ui application and there will be created new session that doesn't matter = that's one session per application for user (if sessioin could be shared between these two apps that's great).
But if I navigate to admin with new session then to ui with new session and then back to the admin with new session there will be a lot of session in sessionstore. So I just want to avoid this "unnecessary" session creation while navigating between applications.
I don't know if you get what I mean, if not I will explain in more detail.

I don't get it. All those sessions are necessary. You just have to avoid them sharing storage (I think that's the bug).

@dsyer when I'm navigating between apps:

  1. on gateway new session g1 is created
  2. navigating to ui new u1 session is created
  3. navigating to adminapp new a1 session is created
  4. navigating to ui new u2 session is created
  5. navigating to adminapp new a2 session is created

so I was thinking about steps 4 and 5 when I want to reuse sessions created in earlier steps instead of creating new one. So in step 4 use u1 and in step 5 use a1. Is this somehow possible or I'm thinking wrong about it?

All those sessions are necessary. You just have to avoid them sharing storage (I think that's the bug).

Why avoid sharing storage? As I read that could be potential security issue. I'm still thinking about high availability and failover in case something goes wrong. Would it be ok if I store sessions to separate spaces in redis per application?

I believe there is a bug which mixes up the storage between your gateway and your auth server in redis (since the two systems work fine if only one of them has a redis session). That's not (just) a security issue, it's a bug that prevents you from authenticating in your gateway. Properly sharing sessions between the auth server and the gateway might fix that, but it might even be better if they are separate anyway (which we know works fine).

There is always a security risk with sharing session data, but you can make rational decisions about that. If two processes are part of the same app logically and you control them both, then it's not completely crazy to share a session between them. But on the other hand, since the only thing I would store in the session is security data (like an access token for instance), there isn't necessarily a huge benefit in doing so. Shared session data is better as a means of scaling up a single process to many of the same type IMO, not so much for creating large distributed apps.

May I ask about the actual fix here? I am having the same exact behavior, but the oauth server does not share any session with client application.
But, client application is using redis as session store.

It seems that the OAuth2ClientContext preserved state is always empty when the user is redirected from the login screen. Because of that, I am getting a 401, because the request as a state, but not the context?

I took a look at what is stored in redis, but the keys only shows the SPRING_SECUIRTY_SAVED_REQUEST without anything related to the state?

I don’t think that’s the same problem then is it? Does the code from the “complete” sample work for you? If it does then there isn’t really an issue with the guide, I guess.

Yes indeed, I just realized that when redis is configured to persist as json, it will miss some of the objects to be serialized, compared to the default jdk serialization strategy. The context being one of them.