Implement Spring OAuth2 using JDBC with JWT and customize existing grant flows using XML based configuration

后端 未结 1 1166
一向
一向 2021-02-11 03:16

I started to use Spring OAuth2 and in the process, I struggled to find relevant tutorials and content mostly because of the following

  • I did not want to use Spring
1条回答
  •  走了就别回头了
    2021-02-11 03:54

    I started off with basic setup of Spring Security framework using mainly XML configuration.

    Spring Security OAuth2 I did a ton of google searches and looked at a bunch of repositories including the main Spring OAuth 2 repo.

    I started off with OAuth2 XML Configs and I needed to make changes in order to

    1. Organize, clean up the spring-security.xml OAuth2 config file
    2. Instruct Spring OAuth2 to lookup a different (customized) user and role tables from database
    3. Instruct Spring OAuth2 to lookup a different (customized) oauth_client_details table (with extra columns)
    4. Use JWT Tokens and make required changes to customize the JWT Token to include custom claims
    5. Develop custom implementation of OAuth2RequestValidator and inject it to the Token endpoint using XML configuration
    6. Disable a grant flow
    7. Enable verify token endpiont /oauth/check_token and validate JWT token custom claims

    And, I have achieved the above as follow

    1. Organize and clean up spring-security.xml

    The default spring-security.xml linked above comes with the following assumptions

    1. Both Authorization server and Resource server are in the same machine.
    2. Use in-memory users
    3. Use in-memory oauth clients

    I would like to keep the Authentication and Resource server separate hence, I stripped off all the protected endpoints configs and the in-memory user-service and oauth-client-details.

    2. Instruct OAuth2 to lookup different user and roles database table

    To achieve this, I made sure that

    1. MyUser object implements the following org.springframework.security.core.userdetails.UserDetails and overrides the getAuthorities which return a collection of GrantedAuthority
    2. MyRole object implements the org.springframework.security.core.GrantedAuthorityand override the method getAuthority

    I now need to inform the Spring OAuth2 about the above customised MyUser and MyRole and in order to do that, I needed to do the following

    1. Provide custom implementation for org.springframework.security.core.userdetails.UserDetailsService interface and override the inherited loadByUsername method to query my custom user database table and retrieve the user with its roles and construct an instance of org.springframework.security.core.userdetails.User and return it.
    2. Create a of the above custom implementation and let's call it MyUserDetailsService and place it in spring-security.xml config file. Something as follow

    Just defining a bean is once again not enough, we have to tell Spring OAuth2 to use the MyUserDetailsService and to do that we need to inject the myUserDetailsService into Spring OAuth2's default authentication provider or our own authentication provider that will then get passed to authentication-manager element, as shown below

    
          
          
          
        
    

    Finally we need to inject the above Authentication Provider into the Authentication Manager specified in spring-security.xml as shown below

     
    
            
                
                    
                    
                
            
        
    

    Change the above with below

     
    
            
        
    

    And now to use the above Authentication Manager which uses, pass it to a grant flow in the as follow

    
    

    3. Instruct Spring OAuth2 to look up different oauth_client_table

    If you look closely at the default spring-security.xml from the official Spring OAuth2 repo linked above, you will see the following chain of references

    clientCredentialsTokenEndpointFilter bean

    references

    clientAuthenticationManager bean

    references

    clientDetailsUserDetailsService

    references

    clientDetails

    And clientDetails is nothing but a list of fixed oauth clients declared at the end under tag.

    Alright, so the objective is to instruct Spring OAuth2 to read and store oauth_clients to/from a database. If we do not need to customize the the default spring oauth database tables to meet our specific needs, then its quite easy and the process is as follow

    1. Setup the default Spring OAuth2 database tables as shown in this link
    2. In the spring-security.xml change the clientDetailsUserDetailsService to pass the default Spring OAuth2 org.springframework.security.oauth2.provider.client.JdbcClientDetailsService and declare the same as a bean, as shown below.

    Defautlt Spring OAuth2 JdbcClientDetailsService bean definition

    
          
             
             
          
       
    

    and now the clientDetailsUserDetailsService bean should look as follow

    
            
        
    

    And everything else the stays the same. The above changes, will instruct the Spring OAuth2 to read oath_clients from the database (oauth_client_details) rather than hard-coded xml config.

    What if, you wanted to modify the default oauth_client_details table to adda few custom columns to meet your specific needs, will in that case you need to make a different set of changes. So we have the following objective

    1. Add extra columns to the default oauth_client_details table

    given the following (in the default spring-security.xml)

    clientCredentialsTokenEndpointFilter bean

    references

    clientAuthenticationManager bean

    references

    clientDetailsUserDetailsService

    references

    clientDetails

    We need to transform the above workflow (or chain of references) in order for Spring OAuth2 to be able to access our new customized oauth_client_details database table. So the new workflow or chain of references should be similar to below

    clientCredentialsTokenEndpointFilter bean (no change)

    references

    clientAuthenticationManager bean (no change)

    references

    clientDetailsUserDetailsService (to be updated)

    references

    clientDetails (to be replaced)

    And to achieve the above, lets move from bottom to top. The default Spring OAuth2 uses org.springframework.security.oauth2.provider.ClientDetails to load oauth_client from the default oauth_client_details table. We need to provide a custom implementation for org.springframework.security.oauth2.provider.ClientDetails by implementing it hence, something as follow

    public class MyClientDetails implements ClientDetails { ... }
    

    Before we move on, I just wanted to mention that Spring OAuth2 already provides an implementation for the above interface meaning that we could make our life a lot easier by actually extending the only implementation of the above interface (ClientDetails) which is org.springframework.security.oauth2.provider.client.BaseClientDetails rather than implementing it from the beginning (leverage all that can be done from BaseClientDetails and add our own custom fields) hence, the MyClientDetails looks as follow then

    public class MyClientDetails extends BaseClientDetails {
          //fields representing custom column for oauth_client
          //getters and setters 
          //make sure to call super() in the inherited constructors, before
          //setting custom fields.
    }
    

    Alright, now that we have our own ClientDetails object, just like #2 we need to implement our own JdbcClientDetailsService that will get injected to clientDetailsUserDetailsService. In order to implement our own JdbcClientDetailsService let's look at the signature of the default method

    public class JdbcClientDetailsService
    extends Object
    implements ClientDetailsService, ClientRegistrationService
    

    As you can see, the above class implements ClientDetailsService & ClientRegistrationService interfaces. While implementing our own JdbcClientDetailsService I do not recommend extending the default JdbcClientDetailsService because there are quite a few private class variables (and methods) which will not be inherited hence, let's use the default implementation to write our own.

    public class MyJdbcClientDetailsService implements ClientDetailsService, ClientRegistrationService {
       //override loadClientByClientId method, query the custom
       //oauth_client_details database table and populate the 
       //MyClientDetails object created above and return it. 
    
       //Implement other methods to CRUD oauth_clients  
    }
    

    Now that you implemented MyJdbcClientDetailsService create a bean for it in spring-security.xml, in the following lines

    Now inject the above bean to clientDetailsUserDetailsService, and looking into default spring-security.xml, we have the following

    
    
            
        
    

    And the above will need to be changed to as follow

    
            
        
    

    There we go, now the chain looks as follow after specifying our own ClientDetailsService:

    clientCredentialsTokenEndpointFilter bean (no change)

    references

    clientAuthenticationManager bean (no change)

    references

    clientDetailsUserDetailsService (ref updated)

    references

    myJdbcClientDetailsService (our customized client details service)

    The above instructs the Spring OAuth2 to look up a customized database table for oauth_client details.

    4. Using JWT Token and want to add extra claims

    JWT Token or JSON Web Token can be used among three different actors of OAuth2 (Authorization server actor, Resource Server actor, Client actor) in order to communicate with each other in performing different actions relating to authorization and access of protected resources. The JWT token is used by Authorization server and it is signed using a public/private key and the objective is to make sure that the content of the JWT token do not get updated (unless someone have access to the private key). The basic workflow can be as follow (Let's assume password grant flow)

    1. Client actor makes a request on behalf of the Resource owner actor (user) to the Authorization server actor asking for an OAuth2AccessToken
    2. Authorization server actor verifies that the requesting client is trusted or not (by checking the requesting clients passed client_id and client_secret - the MyJdbcClientDetailsService is used to lookup the client), Once the client is verified and client secret matched, then it will verify the username and password (this could use the MyUserDetailsService to lookup the user in our database user table) that's also been passed by the Client actor (on behalf of user). If a user is found, then its password is matched (maybe using a custom Password encoder - i.e. BCrypt recommended due to slow hash function) and if all well, then it will retrieve the User's role and construct a JWT token, then sign it with it's own public/private key, store the JWT Token with client details in the database (oauth_access_token default table) and return a copy of the JWT Token to the requesting client.
    3. The Client actor then sends the user's request to the Resource server asking for a protected resource passing along the JWT token. The Resource server validates that the JWT is valid and not tampered, and then accordingly serve the Client actor.

    Above briefly describes how the JWT token is used by OAuth2. There is a ton of more details on JWT and varieties of JWT out there (signed, signed and encrypted, etc etc).

    For more information on JWT implementation of Spring OAuth2, see the official repo here and JWT webpage.

    Let's see, we have the following JWT

    {
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true
    }
    

    and we would like to include a set of scopes to the above token, so the Resource server actor know whether this user is allowed to make this request or not. The change could look as follow

    {
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true, 
      "scope": "read write delete"
    }
    

    To add custom claims (each of above key/value is referred to as claims), we need to create a class MyTokenEnhancer which implement org.springframework.security.oauth2.provider.token.TokenEnhacer interface and override it's enhance method OR extend org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter which in turn implements the default TokenEnhancer and override it's enhance method.

    By overriding the enhance method, you can add custom claims to the OAuth2AccessToken. Please see the above linked repo for more details on this.

    5.Develop custom implementation of OAuth2RequestValidator and inject it to the Token endpoint

    For me, the oauth clients send a customized OAuth2Request and I needed more than the deafult DefaultOAuth2RequestValidator implementation hence, I had to write my own and implemented the org.springframework.security.oauth2.provider.OAuth2RequestValidator. Something in the following lines

    public class MyOAuth2RequestValidator implements OAuth2RequestValidator {
           //override the necessary methods
    }
    

    Now that we have created our own custom MyOAuth2RequestValidator how should we instruct the Token endpoint to use it using XML configuration? Well, this one took me a little time for figure it out. Looking into the default spring-security.xml we have the following

    
            
            
            
            
            
        
    

    When we make an OAuth2 request to the /oauth/token endpoint, the request is mapped to ClientCredentialsTokenEndpointFilter first, and from there at some stage the request is delegated to org.springframework.security.oauth2.provider.endpoint.TokenEndpoint and if you look at the source code TokenEndpoint you will see the following

    private OAuth2RequestValidator oAuth2RequestValidator = new DefaultOAuth2RequestValidator();
    

    Now to replace that with our Custom validator MyOAuth2RequestValidator we need to do the following

    1. Create a bean that references our custom MyOAuth2RequestValidator and name give it id of "myOAuth2RequestValidator"
    2. Update the in the spring-security to use our custom validator from #1

    See below the updated authorization-server tag

    
                
                
                
                
                
            
    

    And that's it. Now, every time the TokenEndpoint is hit, it will use our custom validator instead of default one.

    6. Disable a grant flow

    We might not use all of the available grant flows hence, make sense to disable those not used. Let's say, I don't want to use refresh-token grant flow (just an example) and to disable it we have to update the

    The authorization server with disabled refresh-token grant flow looks as follow

    You can also disable other flows. Obviously you can get away with not disabling it because, if you do not support let's say Authorization Code grant flow then simply do not register an oauth_client that has a value of authorization_code in it's oauth_client_table (if default oauth table)'s authorized_grant_types column.

    Also to share with you an experience that helped me understand Spring OAuth2, I actually debugged the Spring OAuth2 classes to understand how each workflow worked and kept track of all the classes that got hit during different grant flows. It's helpful to give you an overall understanding of how Spring OAuth2 works and then it will become less painful to implement your own OAuth2 implementation on top of Spring OAuth2.

    7. Enable Verify token endpoint and Validate JWT Custom claims

    If the Resource and Authorization server are not in the same server and not sharing the same database, then Resource server requires to double check with Authorization server after it receives a request for a resource to make sure that the token is still valid (this is useful when tokens don't expire too soon) and has the right permissions/claims. In order to achieve this Spring OAuth provides Check Token Endpoint that can be enabled by adding the following

    check-token-enabled="true"
    

    to the element in the XML configuration file. Once the above is added, a POST request to {server-url}/oauth/check_token with a form parameter with key token and value JWT access token will instruct the Authorization server to validate that the token is valid. The default implementation does the default checks such as

    • Expiry date
    • Signature verification
    • Etc

    And you will have to do a little bit of customization to validate your custom claims. See my other post in here for more details.

    0 讨论(0)
提交回复
热议问题