Before we jump into How to implementAuthentication & Authorization into any application, let's familiarise ourselves with some key concepts.
Concept | Definition |
---|---|
Authentication | is a means to know Who is trying to access your application/services/resources/web pages. |
Authorization | is a means to know what an authorized user can access? Think of roles and privileges. |
OAuth / OAuth2.0 | is a token based Authorization framework that provides access to resources which may or may not be limited based on the privileges assigned to the user who generated a token. The token then is used to access resources. It decouples authentication and authorization from an application and delegates authentication to the service which manages accounts. |
JWT | is a JSON Web Token, which is just a way of representing your token as JSON with a lot more information. It has a Header, a Body, and a Signature part, the former 2 being encoded and the Signature part being encrypted. The signature part actually keeps it secure. JWTs have an expiry, and you can send a lot of other details pertaining to a user using claims. |
OAuth2.0 + JWT | When we combine the two, we actually want to your OAuth2.0 but generate JWT tokens from the OAuth2 server. So it's just a way of enhancing the tokens produced by OAuth2.0 Server. |
Microservices | used synonymously with Microservices Architecture, is a variant of the service-oriented architecture (SOA) architectural style that structures an application as a collection of loosely coupled services. In a microservices architecture, services should be fine-grained and the protocols should be lightweight. A Single microservice however is a single responsibility a service must have and is relatively small, easy to manage, can be owned by a small team/single developer and does not rely/depend on any other service to finish its primary responsibility. |
With the more and more popularity in Microservices architecture, token-based authentication & authorization is gaining a fair share of attention as well. Microservices follow the principles of Single Responsibility and independent units of work. They are numerous in number because their whole existence is based upon a single job they must do. When combined and orchestrated, they may be part of a workflow or a business requirement and may work on more than one responsibility together.
Since the applications we are talking about here are web applications, we can not ignore user sessions that we have long relied on. HTTP by nature is stateless, so managing sessions of a logged in user at the application level, via cookies, or using sticky sessions, or session replication is only natural, after all, we can not ask users to log-in to our system over and over again to access a resource on our application. But with tokens in place, users get to hold their tokens, usually in their browsers or rest-clients and use them until they expire and generate new tokens when they need one.
OAuth2
Another way to understand an independent authorization server is to think of it as a dispenser, which just verifies a user and dispenses a token. The token is then sent to a resource server which verifies if the token bearer should be allowed to access a resource.
Different Roles in OAuth2 framework.
Term | Definition |
---|---|
Authorization Server | or, OAuth2 Server, authenticates and authorizes a user and generates/dispenses a token. |
Resource Server | or, your application where you expose your endpoints/resources. |
Resource Owner | a User which is authorized to access an endpoint/resource. |
Client | a client which is a registered client/application allowed to access Authorization Server on behalf of the User. In other words, Users of a certain server has allowed a client application to access their login information from the authorization server. |
With above roles explained, one can say that
OAuth2 tokens are generated by Clients on behalf of Users by calling an exposed endpoint and providing valid credentials of client and/or user, and/or any other variant.
OAuth2 Grant Types
These are actually various ways of using/requesting a token from the OAuth2 server. Some may need just client crendentials, others may need client and user credentials, others may rely on form based authentication and yet another can rely on a refresh token (which indicates this user's token expired and user only means to refresh the token).
Grant Type | Discussion |
---|---|
Authorization Code | used with server-side Applications i.e. inter-service communication. |
Implicit | used on apps that are usually on trusted devices i.e. user's cellphone/laptop. |
Password | Used with trusted applications where client and user credentials are used. |
Client Credentials | when only client credentials are enough to access a resource, also server side when you trust clients. |
Refresh Token | when you already have a token and a corresponding refresh token. The refresh token then can be used to renew the lease. |
JWT
JWT fills in the gap by securing tokens and eliminating the need of a resource server to communicate with OAuth2 authorization server again to verify the token previously generated by it. Without JWT, the token generated by OAuth2 authorization server (token dispenser), needs to be verified everytime it is used.
JWT Token
Term | Definition |
---|---|
Header | the header part of this JSON token contains values for the type of token, and hashing algorithm used to encode it. |
Body/Payload | contians claims (custom claims like user_id, roles, date of birth, signer, verifier, etc..), expiry date/time. |
Signature | is the encrypted part, which can only be decrypted with a matching key/public key. |
OAuth2.0 Server Implementation
pom.xml
<groupId>com.demo</groupId>
<artifactId>OAuth2Server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>OAuth2Server</name>
<description>OAuth2 With JWT Server</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
(html comment removed: https://mvnrepository.com/artifact/com.google.code.gson/gson )
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
OAuth2ServerApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
@SpringBootApplication
@EnableAuthorizationServer
public class OAuth2ServerApplication {
public static void main(String[] args) {
SpringApplication.run(OAuth2ServerApplication.class, args);
}
}
OAuth2Config.java
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.builders.InMemoryClientDetailsServiceBuilder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
@Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Value("${oauth2.clients}")
private String oauth2Clients;
@Value("${oauth2.jwt.private.key}")
private String privateKey;
@Value("${oauth2.jwt.public.key}")
private String publicKey;
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).tokenStore(tokenStore())
.accessTokenConverter(tokenEnhancer());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
//.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()").passwordEncoder(passwordEncoder());
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
List<OAuthRegisteredClient> oauthClients = new Gson().fromJson(oauth2Clients,
new TypeToken<ArrayList<OAuthRegisteredClient>>() {
}.getType());
InMemoryClientDetailsServiceBuilder inMemory = clients.inMemory();
for (OAuthRegisteredClient client : oauthClients) {
inMemory
.withClient(client.getClientId())
.secret(passwordEncoder().encode(client.getClientSecret()))
.scopes(client.getScopes().split(","))
.authorizedGrantTypes(client.getGrantTypes().split(","))
.accessTokenValiditySeconds(Integer.parseInt(client.getAccessTokenValidity()))
.refreshTokenValiditySeconds(Integer.parseInt(client.getRefreshTokenValidity()));
}
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAccessTokenConverter tokenEnhancer() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(privateKey);
converter.setVerifierKey(publicKey);
return converter;
}
@Bean
public JwtTokenStore tokenStore() {
return new JwtTokenStore(tokenEnhancer());
}
}
OAuth2ServerConfig.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomDetailService customDetailsService;
@Override
@Autowired
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.antMatchers("/oauth/token/").permitAll()
.and().formLogin()
.and().httpBasic()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
CustomDetailService.java
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
@Service
public class CustomDetailService implements UserDetailsService {
@Value("${oauth2.users}")
private String oauth2Users;
@Override
public User loadUserByUsername(final String userName) throws UsernameNotFoundException {
try {
List<OAuthRegisteredUser> users = new Gson().fromJson(oauth2Users,
new TypeToken<ArrayList<OAuthRegisteredUser>>() {
}.getType());
User customUser = null;
for (OAuthRegisteredUser user : users) {
if (user.getUserId().equalsIgnoreCase(userName)) {
String[] roles = user.getRoles().split(",");
List<GrantedAuthority> authorities = new ArrayList<>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
customUser = new User(user.getUserId(), user.getPassword(), authorities);
break;
}
}
return customUser;
} catch (Exception e) {
e.printStackTrace();
throw new UsernameNotFoundException("User " + userName + " was not found in the json");
}
}
}
Client DTO (OAuthRegisteredClient.java) for data/json manipulation
public class OAuthRegisteredClient {
private String clientId;
private String clientSecret;
private String grantTypes;
private String scopes;
private String accessTokenValidity;
private String refreshTokenValidity;
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getClientSecret() {
return clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public String getGrantTypes() {
return grantTypes;
}
public void setGrantTypes(String grantTypes) {
this.grantTypes = grantTypes;
}
public String getAccessTokenValidity() {
return accessTokenValidity;
}
public void setAccessTokenValidity(String accessTokenValidity) {
this.accessTokenValidity = accessTokenValidity;
}
public String getRefreshTokenValidity() {
return refreshTokenValidity;
}
public void setRefreshTokenValidity(String refreshTokenValidity) {
this.refreshTokenValidity = refreshTokenValidity;
}
public String getScopes() {
return scopes;
}
public void setScopes(String scopes) {
this.scopes = scopes;
}
}
User DTO (OAuthRegisteredUser.java) for data/json manipulation
public class OAuthRegisteredUser {
private String userId;
private String password;
private String roles;
private String grantTypes;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRoles() {
return roles;
}
public void setRoles(String roles) {
this.roles = roles;
}
public String getGrantTypes() {
return grantTypes;
}
public void setGrantTypes(String grantTypes) {
this.grantTypes = grantTypes;
}
}
Configuration application.properties
server.port=9999
security.oauth2.resource.filter-order=3
logging.level.org.springframework.security=DEBUG
logging.level.ROOT=INFO
oauth2.clients=[ \
{"clientId":"test", "clientSecret":"test", "grantTypes":"password,refresh_token,implicit", "accessTokenValidity"="2000", "refreshTokenValidity"="2000", "scopes"="read,write"}, \
{"clientId":"test1", "clientSecret":"test1", "grantTypes":"password, implicit", "accessTokenValidity"="4000", "refreshTokenValidity"="4000", "scopes"="read"} \
]
oauth2.users=[ \
{ "userId":"user1", "password":"password1", "roles":"ROLE_ADMIN,ROLE_USER"}, \
{ "userId":"user2", "password":"password2", "roles":"ROLE_USER"} \
]
oauth2.jwt.private.key=-----BEGIN RSA PRIVATE KEY----- \
MIIEogIBAAKCAQEAr/SxoBLtgJLJlih0xY18eotVdK9cBWwzyf7tMwN4lABq3A+t \
IwpwNOaMxDBqAdqyyOUJd340GfVChIKsLnlfKACg8uxX/uHk8+Vb2k2sQmD9hf7n \
+oyKK6C4By4t00G5SaJzu5DrOLRNmCEpTZC/z33JvubDw5ul6iz4b9HpeJk4ShBs \
BJJLNWKX07M2i5TW1KQUbKTGcDdrrTN1FyilH4T6SNuKWmcFQAs1gWk92oTyq3X3 \
KikzZ7bnTGY4i2/DquEjmQj075n7iqxZAnMaDg1uiD7JGlcOxDlzh6qVVCulCVyY \
fwdMcBDErGvn35Zu0hmfsuTQBIP9EmjRjhI4bwIDAQABAoIBAGn71p3XqM0AHYJf \
ve5cibT6hi+xjJkLkqpB7jgDCDcFhVgl7SClIe8LS5DEpKWXSIcvRiN+lf1ouHc+ \
il2357QdpNSNvXceM50g8tGsZWzaJPr4DlzhGDytIwvYKewMs4GB43oAEV7nY7GV \
j95TqOVfHAzrzNDeyPIjwkUASEPF7dSJZzwku5JXaueNRoXGHkX++YBuIJe/aG+N \
FOePzJ+fVGX+SAR2L3gznU2SShE7IfN78KAWhuuujM3VZQ+ndGdBaIWBImyYch8G \
gjjH63DH9XyZoOe6RWdhlQrzuzfojEU0ASwOuwluBiZkKrjrUCELNYvy0HwlO0hW \
n871IFECgYEA6OYFOZsnimD1Fdh7IHGTltLS20YKXM6lkXIbFRheUi7hKgtdCLyC \
jjGaCmLIcpdj7glnFUo6kWw9d1zDKf38ZcfThpkir6gHZTjQEWNlw7DRkVpoVAjN \
f9QRjssy7lVAVoLS0N7tbnY1vNqqMRALTKiAkimBkHOUCADQDy7peFcCgYEAwWi7 \
IGzNbRfucXhtE2P2py2XGAqpRRabTNK4axFTo37R5YQKlbPtdDGn3FMC00s9lXe3 \
rsc/slfJdd+78OBXDw/sx9Pwae9qLiVyQMPAHh0+GykBbKYE+cuvAhdCr/DrWDCi \
cl165ZT9/pnv7svSIC6lx4Jk9swDGT0wtgosEakCgYBh+4PvPGtiOHBkBHOorp/d \
ME0tm0X4zaeyjUL/mQT1pmtseONnLYfvssaP4AdcznxoRORtfJEvZU5mzUv+erCZ \
UXyYgAyrwhF6cHB5IExMDDPe5x5FXH64bMwGO07uHke3Hcr7vQ4/DtSE46n+gjV+ \
xtVwqK2tzDJXO6BNAvuOeQKBgG2O6hFgEK3vlH5qfT8yrRHeXWyc/W3JdId8BkOA \
OSSwPi4JMgXDOotLZY8E5uDSCjyU6FP1QjvARrqi1k+IhUhe+P3pLJKVsEwbojDV \
sgpc/kVcYRYupTrrCmPg0UHg8y+tvReTpH5Qe+CxSpy2kHzyb64oSxESV98ALouw \
D26hAoGAAauIYsWXP6u2DGQmeVJJ7VMXmuJj6zbi1Kz2b7cPqTS1JcQ9b4WgqKbW \
7Qk8SdTjpDBvKL+CZHmLmY7M+A/uThfL3oi1S5E2EwuWvaRHa1XGqp4RfI8zoqcB \
xwoumtMhMTJWtE5vp7eGhV+BReTBYrOeSLCyewRoEIJPsI3Xt8c= \
-----END RSA PRIVATE KEY-----
oauth2.jwt.public.key=-----BEGIN PUBLIC KEY----- \
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/SxoBLtgJLJlih0xY18 \
eotVdK9cBWwzyf7tMwN4lABq3A+tIwpwNOaMxDBqAdqyyOUJd340GfVChIKsLnlf \
KACg8uxX/uHk8+Vb2k2sQmD9hf7n+oyKK6C4By4t00G5SaJzu5DrOLRNmCEpTZC/ \
z33JvubDw5ul6iz4b9HpeJk4ShBsBJJLNWKX07M2i5TW1KQUbKTGcDdrrTN1Fyil \
H4T6SNuKWmcFQAs1gWk92oTyq3X3KikzZ7bnTGY4i2/DquEjmQj075n7iqxZAnMa \
Dg1uiD7JGlcOxDlzh6qVVCulCVyYfwdMcBDErGvn35Zu0hmfsuTQBIP9EmjRjhI4 \
bwIDAQAB \
-----END PUBLIC KEY-----
Note 1: If you notice above, instead of database, the UserDetailService is supposed to fetch users from the hard-coded JSON provided in properties file, and same is the case with clients. You can easily switch it to database and fetch from your database. I find it simple to use JSON for such demos.
Note 2: Private and Public key can easily be generated on your command prompt/console by using rsa tool i.e.
- generate key
openssl genrsa -out jwt.pem 2048
- display private key
openssl rsa -in jwt.pem
- display public key
openssl rsa -in jwt.pem -pubout
OAuth2 Resource Server Implementation
pom.xml
<groupId>com.demo</groupId>
<artifactId>SpringSecurityDemo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>SpringSecurityDemo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath /> (html comment removed: lookup parent from repository )
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
application.properties
logging.level.ROOT=INFO
server.port=8080
oauth2.jwt.public.key=-----BEGIN PUBLIC KEY----- \
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/SxoBLtgJLJlih0xY18 \
eotVdK9cBWwzyf7tMwN4lABq3A+tIwpwNOaMxDBqAdqyyOUJd340GfVChIKsLnlf \
KACg8uxX/uHk8+Vb2k2sQmD9hf7n+oyKK6C4By4t00G5SaJzu5DrOLRNmCEpTZC/ \
z33JvubDw5ul6iz4b9HpeJk4ShBsBJJLNWKX07M2i5TW1KQUbKTGcDdrrTN1Fyil \
H4T6SNuKWmcFQAs1gWk92oTyq3X3KikzZ7bnTGY4i2/DquEjmQj075n7iqxZAnMa \
Dg1uiD7JGlcOxDlzh6qVVCulCVyYfwdMcBDErGvn35Zu0hmfsuTQBIP9EmjRjhI4 \
bwIDAQAB \
-----END PUBLIC KEY-----
SpringSecurityDemoApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringSecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityDemoApplication.class, args);
}
}
OAuth2Config.java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
@EnableResourceServer
@EnableWebSecurity
public class OAuth2Config extends ResourceServerConfigurerAdapter {
@Value("${oauth2.jwt.public.key}")
private String publicKey;
@Override
public void configure(ResourceServerSecurityConfigurer config) {
config.tokenServices(tokenServices());
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new CustomJwtTokenConverter();
converter.setVerifierKey(publicKey);
return converter;
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
}
CustomJwtTokenConverter.java (The Token Enhancer)
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
public class CustomJwtTokenConverter extends JwtAccessTokenConverter {
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication auth = super.extractAuthentication(map);
Map<String, String> parameters = new HashMap<String, String>();
Set<String> scope = auth.getOAuth2Request().getScope();
Set<String> resources = auth.getOAuth2Request().getResourceIds();
String clientId = (String) map.get(CLIENT_ID);
parameters.put(CLIENT_ID, clientId);
if (map.containsKey(GRANT_TYPE)) {
parameters.put(GRANT_TYPE, (String) map.get(GRANT_TYPE));
}
Collection<? extends GrantedAuthority> authorities = null;
if (map.containsKey(AUTHORITIES)) {
@SuppressWarnings("unchecked")
String[] roles = ((Collection<String>)map.get(AUTHORITIES)).toArray(new String[0]);
authorities = AuthorityUtils.createAuthorityList(roles);
}
OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resources, null, null,
null);
return new OAuth2Authentication(request, auth);
}
}
OAuth2MethodSecurityConfiguration (To allow expressions for role based restrictions on services)
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}
HelloWorldService.java
@RestController
public class HelloWorldService {
@PreAuthorize("#oauth2.hasScope('read')")
@GetMapping(path = "/user", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserDto> getUser() {
return new ResponseEntity<>(new UserDto("dummy", "Hello Dummy!"), HttpStatus.OK);
}
@PreAuthorize("#oauth2.hasAnyScopeMatching('write') and #oauth2.clientHasRole('ROLE_ADMIN')")
@PostMapping(path = "/user", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserDto> addUser(@RequestBody UserDto user) {
user.setMsg("successfully added.");
return new ResponseEntity<>(user, HttpStatus.OK);
}
}
Simple UserDto for JSON response.
public class UserDto {
private String user;
private String msg;
public UserDto() {
super();
}
public UserDto(String user, String msg) {
this.user = user;
this.msg = msg;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
##Project Structure
###OAuthServer
###ResourceServer
Running Your Application
Run Authorization Server as java application
Run Resource Server as Java Application
Request access token from Authorization Server (pass client id and secret as basic auth) and send POST request
Access Resources in Resource Server