OAuth 2.0 using Spring Security + WSO2 Identity Server - java

I'm developing a web application to expose a number of RESTful services secured by OAuth 2.0. Here is the planned architecture:
1- OAuth Authorization Provider: WSO2 Identity Server (IS)
2- OAuth Resource Server: Java web application using the following technologies:
Jersey (to implement and expose the web services)
Spring Security (to implement the OAuth Resource Server part)
I've seen several examples (ex1, ex2, ex3, etc...) on how to secure RESTful services using WSO2 IS as an authorization server + WSO2 ESB as a resource server. This is NOT what I need in my case.
Unfortunately, the interaction between the authorization server and the resource server is beyond the scope of the OAuth2 RFC. So, I couldn't find much about how should it look like.
Here are my questions:
How to configure spring security to act as a resource server to validate an access token issued by an external OAuth provider (e.g. WSO2 IS)?
How should the resource server identify the scope of a given access token?
How to identify the resource owner given an access token from WSO2 IS?
Thanks

After doing some research, I figured out how to do it. The solution is divided into 2 main parts: WSO2 IS configuration & Resources server configuration.
The basic scenario goes as follows:
1- A client (e.g. mobile app) consume a secured resource (e.g. web service) by sending a request to the resources sever (Java web application in my case).
2- The resources server validates the "Authorization" header in the request and extracts the access token.
3- The resources server validates the access token by sending it to the authorization server (WSO2 IS).
4- The authorization server responds with validation response.
5- The resources server validates the response and decides whether to grant or deny access to the requested resource.
In my demo, I used WSO2 IS 5.0.0 and Spring security 3.1.0.
1- WSO2 IS Configuration
WSO2 IS will act as the authorization server. So, it should be configured to support OAuth 2.0. To do so, a new service provider should be added and configured as follows:
(a) Login to WSO2 IS management console.
(b) Add a new service provider and give it a name and description.
(c) Under Inbound Authentication Configuration >> OAuth/OpenID Connect Configuration >> Click Configure.
(d) Configure OAuth 2.0 provider as shown in the below screenshot and click Add. We'll need Password grant type which maps to Resource Owner Password Credentials grant type. It is best suited for my case (securing web services).
(e) Under OAuth/OpenID Connect Configuration, you'll find OAuth Client Key and OAuth Client Secret generated. They are used along with username, password, and scope to generate access tokens.
2- Resources Server Configuration
As mentioned earlier, the demo Java web application will act as Resources server and client at the same time. To act as resources server, Spring security needs to know how to validate access tokens. So, a token services implementation should be provided.
(a) Configure spring to act as resources server. Here is a sample configuration:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:oauth2="http://www.springframework.org/schema/security/oauth2"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd
http://www.springframework.org/schema/security/oauth2
http://www.springframework.org/schema/security/spring-security-oauth2.xsd">
<bean id="tokenServices" class="com.example.security.oauth2.wso2.TokenServiceWSO2" />
<bean id="authenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint" />
<security:authentication-manager alias="authenticationManager" />
<oauth2:resource-server id="resourcesServerFilter" token-services-ref="tokenServices" />
<security:http pattern="/services/**" create-session="stateless" entry-point-ref="authenticationEntryPoint" >
<security:anonymous enabled="false" />
<security:custom-filter ref="resourcesServerFilter" before="PRE_AUTH_FILTER" />
<security:intercept-url pattern="/services/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
</security:http>
</beans>
Here, a resource-server that uses a token services implementation TokenServiceWSO2 is configured. The resource-server tag is actually transformed to a security filter. An interception pattern is added to "/services/**" and the resources sever filter is added to the chain.
(b) Implement OAuth 2.0 token services ResourceServerTokenServices. The implementation will take an access token as an input, pass it to OAuth2TokenValidationService service exposed by WSO2 IS, validate the response and return a processed object containing the basic data about the token's issuer, validity, scope, corresponding JWT token, ...
public class TokenServiceWSO2 implements ResourceServerTokenServices {
#Autowired
TokenValidatorWSO2 tokenValidatorWSO2;
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
try {
TokenValidationResponse validationResponse = tokenValidatorWSO2.validateAccessToken(accessToken);
OAuth2Request oAuth2Request = new OAuth2Request(null, null, null, true, validationResponse.getScope(), null, null, null,null);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(validationResponse.getAuthorizedUserIdentifier(), null, null);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
return oAuth2Authentication;
} catch (ApplicationException ex) {
// Handle exception
}
}
public OAuth2AccessToken readAccessToken(String accessToken) {
// TODO Add implementation
}
}
TokenValidatorWSO2 class implements the logic to call WSO2 IS's web service OAuth2TokenValidationService
#Component
public class TokenValidatorWSO2 implements OAuth2TokenValidator{
private static final Logger logger = Logger.getLogger(TokenValidatorWSO2.class);
#Value("${server_url}")
private String serverUrl;
#Value("${validation_service_name}")
private String validationServiceName;
#Value("${comsumer_key}")
private String consumerKey;
#Value("${admin_username}")
private String adminUsername;
#Value("${admin_password}")
private String adminPassword;
private OAuth2TokenValidationServiceStub stub;
private static final int TIMEOUT_IN_MILLIS = 15 * 60 * 1000;
public TokenValidationResponse validateAccessToken(String accessToken) throws ApplicationException {
logger.debug("validateAccessToken(String) - start");
if(stub == null) {
initializeValidationService();
}
OAuth2TokenValidationRequestDTO oauthRequest;
TokenValidationResponse validationResponse;
OAuth2TokenValidationRequestDTO_OAuth2AccessToken oAuth2AccessToken;
try {
oauthRequest = new OAuth2TokenValidationRequestDTO();
oAuth2AccessToken = new OAuth2TokenValidationRequestDTO_OAuth2AccessToken();
oAuth2AccessToken.setIdentifier(accessToken);
oAuth2AccessToken.setTokenType("bearer");
oauthRequest.setAccessToken(oAuth2AccessToken);
OAuth2TokenValidationResponseDTO response = stub.validate(oauthRequest);
if(!response.getValid()) {
throw new ApplicationException("Invalid access token");
}
validationResponse = new TokenValidationResponse();
validationResponse.setAuthorizedUserIdentifier(response.getAuthorizedUser());
validationResponse.setJwtToken(response.getAuthorizationContextToken().getTokenString());
validationResponse.setScope(new LinkedHashSet<String>(Arrays.asList(response.getScope())));
validationResponse.setValid(response.getValid());
} catch(Exception ex) {
logger.error("validateAccessToken() - Error when validating WSO2 token, Exception: {}", ex);
}
logger.debug("validateAccessToken(String) - end");
return validationResponse;
}
private void initializeValidationService() throws ApplicationException {
try {
String serviceURL = serverUrl + validationServiceName;
stub = new OAuth2TokenValidationServiceStub(null, serviceURL);
CarbonUtils.setBasicAccessSecurityHeaders(adminUsername, adminPassword, true, stub._getServiceClient());
ServiceClient client = stub._getServiceClient();
Options options = client.getOptions();
options.setTimeOutInMilliSeconds(TIMEOUT_IN_MILLIS);
options.setProperty(HTTPConstants.SO_TIMEOUT, TIMEOUT_IN_MILLIS);
options.setProperty(HTTPConstants.CONNECTION_TIMEOUT, TIMEOUT_IN_MILLIS);
options.setCallTransportCleanup(true);
options.setManageSession(true);
} catch(AxisFault ex) {
// Handle exception
}
}
}
TokenValidationResponse class holds the basic data returned in token validation response.
public class TokenValidationResponse {
private String jwtToken;
private boolean valid;
private Set<String> scope;
private String authorizedUserIdentifier;
public String getJwtToken() {
return jwtToken;
}
public void setJwtToken(String jwtToken) {
this.jwtToken = jwtToken;
}
public boolean isValid() {
return valid;
}
public void setValid(boolean valid) {
this.valid = valid;
}
public Set<String> getScope() {
return scope;
}
public void setScope(Set<String> scope) {
this.scope = scope;
}
public String getAuthorizedUserIdentifier() {
return authorizedUserIdentifier;
}
public void setAuthorizedUserIdentifier(String authorizedUserIdentifier) {
this.authorizedUserIdentifier = authorizedUserIdentifier;
}
}
3- Client Application Configuration
The last step is to configure the resources to be protected by OAuth 2.0. Basically, configure the web services to be secured with a root URL path "/services/**". In my demo, I used Jersey.
4- Test The Client Application
The last step is to consume the secured web services. This is done by adding Authorization header to the request with value " ", for example "bearer 7fbd71c5b28fdf0bdb922b07915c4d5".
P.S. The described sample is just for clarification purposes. It may be missing some implementations, exception handling, ... Kindly comment for further inquiries.

Related

Extra authentication step for OAuth2/OIDC client authorisations from multiple auth servers

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)
);
}
}

Central auth server with multiple clients using resource owner password credentials oauth flow

I've got the following set up:
Central auth server written with spring boot that is currently working (I can curl and receive an access token, jdbc token store, etc)
Multiple applications owned by the same developer, sharing the same customer base on different domains. IE: John Doe for app1 is the same as John Doe for app2.
I have an existing application (app1 above) that is jsf 2.2 with spring security configured for login purposes. That application works stand alone right now, with it's own user base.
This is the flow I am trying to obtain:
Resource Owner Password Credential OAuth Flow
So we would want:
User goes to app1
User enters user and password into app1 login page
User hits "login"
Some sort of configuration in Spring would then take the loginByUsername request, get access token from the central oauth server
We now have app1 access - the user could have one of three roles (ADMIN, USER, SUPERUSER).
When they go to (say) app1/views/createEntry.xhtml, we would confirm the access token we currently have is still active on the auth server.
The resource server would technically be the resources on the app1 server (right?)
I'm new to this oauth2.0 process (and spring really), but I think this is the flow I want. How do I set this up with Spring Security? I've seen a security setting called oauth2login() that I think is what we COULD want, but I think that is more authorization code flow.
I haven't found a very good example of this using the password flow.
I do trust each of the applications in this process, hence the password flow. We control the network that maintains traffic between the auth server and the other applications.
Edit: SSO isn't an option because of requirements and our customer base. The applications are unique enough that it doesn't make sense, but the user should be able to log into any of our applications with those credentials.
Edit 2: Sorry for second edit. I would like to add that I've added a resource configuration on app1 and it actually seems like it works - I've secured anything /views/* and when I attempt to go their, I get the expected "Full Authentication required" message.
Edit 3: I think I am making some progress -
First, I created a spring component that implements AuthenticationProvider and then overwrote the authenticate method so that I created a ResourceOwnerPasswordResourceDetails object with all my properties (client id, client secret, grant type, scope, etc) and called the authorization server to get a token. My excitement to see my log refresh for the authorization server was high.
Next step I need to figure out is how to generate an extension of org.springframework.security.core.userdetails.User so that I can store the privileges for the user.
Also - I can't quite figure out yet how the token is stored. I know the auth server generates a token and stores in jdbc, but where/how does the token get stored on the client side?
For those that were curious, here is how I set up the authentication provider on my client (app1). I still have issues with the resource server (ill ask a separate question), but here is what I did:
Custom authenticator:
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
private AppUserDAO appUserDAO;
private String accessTokenUri = "http://localhost:8080/oauth/token";
private String clientId = "clientid";
private String clientSecret = "clientsecret";
public AccessTokenProvider userAccessTokenProvider() {
ResourceOwnerPasswordAccessTokenProvider accessTokenProvider = new ResourceOwnerPasswordAccessTokenProvider();
return accessTokenProvider;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
final String username = authentication.getName();
final String password = authentication.getCredentials().toString();
List<String> scopes = new ArrayList<String>();
scopes.add("read");
final ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
resource.setUsername(username);
resource.setPassword(password);
resource.setAccessTokenUri(accessTokenUri);
resource.setClientId(clientId);
resource.setClientSecret(clientSecret);
resource.setGrantType("password");
resource.setScope(scopes);
// Generate an access token
final OAuth2RestTemplate template = new OAuth2RestTemplate(resource, new DefaultOAuth2ClientContext(new DefaultAccessTokenRequest()));
template.setAccessTokenProvider(userAccessTokenProvider());
OAuth2AccessToken accessToken = null;
try {
accessToken = template.getAccessToken();
System.out.println("Grabbed access token from " + accessTokenUri);
}
catch (OAuth2AccessDeniedException e) {
if (e.getCause() instanceof ResourceAccessException) {
final String errorMessage = String.format(
"While authenticating user '%s': " + "Unable to access accessTokenUri '%s'.", username,
accessTokenUri);
throw new AuthenticationServiceException(errorMessage, e);
}
throw new BadCredentialsException(String.format("Access denied for user '%s'.", username), e);
}
catch (OAuth2Exception e) {
throw new AuthenticationServiceException(
String.format("Unable to perform OAuth authentication for user '%s'.", username), e);
}
// Determine roles for user
List<GrantedAuthority> grantList = ...
// Create custom user for the principal
User user = .....
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, null /*dont store password*/, grantList);
return token;
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
Security configuration:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider authProvider;
....
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
}

How to implement REST token-based authentication with JAX-RS and Jersey

I'm looking for a way to enable token-based authentication in Jersey. I am trying not to use any particular framework. Is that possible?
My plan is: A user signs up for my web service, my web service generates a token, sends it to the client, and the client will retain it. Then the client, for each request, will send the token instead of username and password.
I was thinking of using a custom filter for each request and #PreAuthorize("hasRole('ROLE')"), but I just thought that this causes a lot of requests to the database to check if the token is valid.
Or not create filter and in each request put a param token? So that each API first checks the token and after executes something to retrieve resource.
How token-based authentication works
In token-based authentication, the client exchanges hard credentials (such as username and password) for a piece of data called token. For each request, instead of sending the hard credentials, the client will send the token to the server to perform authentication and then authorization.
In a few words, an authentication scheme based on tokens follow these steps:
The client sends their credentials (username and password) to the server.
The server authenticates the credentials and, if they are valid, generate a token for the user.
The server stores the previously generated token in some storage along with the user identifier and an expiration date.
The server sends the generated token to the client.
The client sends the token to the server in each request.
The server, in each request, extracts the token from the incoming request. With the token, the server looks up the user details to perform authentication.
If the token is valid, the server accepts the request.
If the token is invalid, the server refuses the request.
Once the authentication has been performed, the server performs authorization.
The server can provide an endpoint to refresh tokens.
What you can do with JAX-RS 2.0 (Jersey, RESTEasy and Apache CXF)
This solution uses only the JAX-RS 2.0 API, avoiding any vendor specific solution. So, it should work with JAX-RS 2.0 implementations, such as Jersey, RESTEasy and Apache CXF.
It is worthwhile to mention that if you are using token-based authentication, you are not relying on the standard Java EE web application security mechanisms offered by the servlet container and configurable via application's web.xml descriptor. It's a custom authentication.
Authenticating a user with their username and password and issuing a token
Create a JAX-RS resource method which receives and validates the credentials (username and password) and issue a token for the user:
#Path("/authentication")
public class AuthenticationEndpoint {
#POST
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response authenticateUser(#FormParam("username") String username,
#FormParam("password") String password) {
try {
// Authenticate the user using the credentials provided
authenticate(username, password);
// Issue a token for the user
String token = issueToken(username);
// Return the token on the response
return Response.ok(token).build();
} catch (Exception e) {
return Response.status(Response.Status.FORBIDDEN).build();
}
}
private void authenticate(String username, String password) throws Exception {
// Authenticate against a database, LDAP, file or whatever
// Throw an Exception if the credentials are invalid
}
private String issueToken(String username) {
// Issue a token (can be a random String persisted to a database or a JWT token)
// The issued token must be associated to a user
// Return the issued token
}
}
If any exceptions are thrown when validating the credentials, a response with the status 403 (Forbidden) will be returned.
If the credentials are successfully validated, a response with the status 200 (OK) will be returned and the issued token will be sent to the client in the response payload. The client must send the token to the server in every request.
When consuming application/x-www-form-urlencoded, the client must send the credentials in the following format in the request payload:
username=admin&password=123456
Instead of form params, it's possible to wrap the username and the password into a class:
public class Credentials implements Serializable {
private String username;
private String password;
// Getters and setters omitted
}
And then consume it as JSON:
#POST
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public Response authenticateUser(Credentials credentials) {
String username = credentials.getUsername();
String password = credentials.getPassword();
// Authenticate the user, issue a token and return a response
}
Using this approach, the client must to send the credentials in the following format in the payload of the request:
{
"username": "admin",
"password": "123456"
}
Extracting the token from the request and validating it
The client should send the token in the standard HTTP Authorization header of the request. For example:
Authorization: Bearer <token-goes-here>
The name of the standard HTTP header is unfortunate because it carries authentication information, not authorization. However, it's the standard HTTP header for sending credentials to the server.
JAX-RS provides #NameBinding, a meta-annotation used to create other annotations to bind filters and interceptors to resource classes and methods. Define a #Secured annotation as following:
#NameBinding
#Retention(RUNTIME)
#Target({TYPE, METHOD})
public #interface Secured { }
The above defined name-binding annotation will be used to decorate a filter class, which implements ContainerRequestFilter, allowing you to intercept the request before it be handled by a resource method. The ContainerRequestContext can be used to access the HTTP request headers and then extract the token:
#Secured
#Provider
#Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {
private static final String REALM = "example";
private static final String AUTHENTICATION_SCHEME = "Bearer";
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Get the Authorization header from the request
String authorizationHeader =
requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
// Validate the Authorization header
if (!isTokenBasedAuthentication(authorizationHeader)) {
abortWithUnauthorized(requestContext);
return;
}
// Extract the token from the Authorization header
String token = authorizationHeader
.substring(AUTHENTICATION_SCHEME.length()).trim();
try {
// Validate the token
validateToken(token);
} catch (Exception e) {
abortWithUnauthorized(requestContext);
}
}
private boolean isTokenBasedAuthentication(String authorizationHeader) {
// Check if the Authorization header is valid
// It must not be null and must be prefixed with "Bearer" plus a whitespace
// The authentication scheme comparison must be case-insensitive
return authorizationHeader != null && authorizationHeader.toLowerCase()
.startsWith(AUTHENTICATION_SCHEME.toLowerCase() + " ");
}
private void abortWithUnauthorized(ContainerRequestContext requestContext) {
// Abort the filter chain with a 401 status code response
// The WWW-Authenticate header is sent along with the response
requestContext.abortWith(
Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE,
AUTHENTICATION_SCHEME + " realm=\"" + REALM + "\"")
.build());
}
private void validateToken(String token) throws Exception {
// Check if the token was issued by the server and if it's not expired
// Throw an Exception if the token is invalid
}
}
If any problems happen during the token validation, a response with the status 401 (Unauthorized) will be returned. Otherwise the request will proceed to a resource method.
Securing your REST endpoints
To bind the authentication filter to resource methods or resource classes, annotate them with the #Secured annotation created above. For the methods and/or classes that are annotated, the filter will be executed. It means that such endpoints will only be reached if the request is performed with a valid token.
If some methods or classes do not need authentication, simply do not annotate them:
#Path("/example")
public class ExampleResource {
#GET
#Path("{id}")
#Produces(MediaType.APPLICATION_JSON)
public Response myUnsecuredMethod(#PathParam("id") Long id) {
// This method is not annotated with #Secured
// The authentication filter won't be executed before invoking this method
...
}
#DELETE
#Secured
#Path("{id}")
#Produces(MediaType.APPLICATION_JSON)
public Response mySecuredMethod(#PathParam("id") Long id) {
// This method is annotated with #Secured
// The authentication filter will be executed before invoking this method
// The HTTP request must be performed with a valid token
...
}
}
In the example shown above, the filter will be executed only for the mySecuredMethod(Long) method because it's annotated with #Secured.
Identifying the current user
It's very likely that you will need to know the user who is performing the request agains your REST API. The following approaches can be used to achieve it:
Overriding the security context of the current request
Within your ContainerRequestFilter.filter(ContainerRequestContext) method, a new SecurityContext instance can be set for the current request. Then override the SecurityContext.getUserPrincipal(), returning a Principal instance:
final SecurityContext currentSecurityContext = requestContext.getSecurityContext();
requestContext.setSecurityContext(new SecurityContext() {
#Override
public Principal getUserPrincipal() {
return () -> username;
}
#Override
public boolean isUserInRole(String role) {
return true;
}
#Override
public boolean isSecure() {
return currentSecurityContext.isSecure();
}
#Override
public String getAuthenticationScheme() {
return AUTHENTICATION_SCHEME;
}
});
Use the token to look up the user identifier (username), which will be the Principal's name.
Inject the SecurityContext in any JAX-RS resource class:
#Context
SecurityContext securityContext;
The same can be done in a JAX-RS resource method:
#GET
#Secured
#Path("{id}")
#Produces(MediaType.APPLICATION_JSON)
public Response myMethod(#PathParam("id") Long id,
#Context SecurityContext securityContext) {
...
}
And then get the Principal:
Principal principal = securityContext.getUserPrincipal();
String username = principal.getName();
Using CDI (Context and Dependency Injection)
If, for some reason, you don't want to override the SecurityContext, you can use CDI (Context and Dependency Injection), which provides useful features such as events and producers.
Create a CDI qualifier:
#Qualifier
#Retention(RUNTIME)
#Target({ METHOD, FIELD, PARAMETER })
public #interface AuthenticatedUser { }
In your AuthenticationFilter created above, inject an Event annotated with #AuthenticatedUser:
#Inject
#AuthenticatedUser
Event<String> userAuthenticatedEvent;
If the authentication succeeds, fire the event passing the username as parameter (remember, the token is issued for a user and the token will be used to look up the user identifier):
userAuthenticatedEvent.fire(username);
It's very likely that there's a class that represents a user in your application. Let's call this class User.
Create a CDI bean to handle the authentication event, find a User instance with the correspondent username and assign it to the authenticatedUser producer field:
#RequestScoped
public class AuthenticatedUserProducer {
#Produces
#RequestScoped
#AuthenticatedUser
private User authenticatedUser;
public void handleAuthenticationEvent(#Observes #AuthenticatedUser String username) {
this.authenticatedUser = findUser(username);
}
private User findUser(String username) {
// Hit the the database or a service to find a user by its username and return it
// Return the User instance
}
}
The authenticatedUser field produces a User instance that can be injected into container managed beans, such as JAX-RS services, CDI beans, servlets and EJBs. Use the following piece of code to inject a User instance (in fact, it's a CDI proxy):
#Inject
#AuthenticatedUser
User authenticatedUser;
Note that the CDI #Produces annotation is different from the JAX-RS #Produces annotation:
CDI: javax.enterprise.inject.Produces
JAX-RS: javax.ws.rs.Produces
Be sure you use the CDI #Produces annotation in your AuthenticatedUserProducer bean.
The key here is the bean annotated with #RequestScoped, allowing you to share data between filters and your beans. If you don't wan't to use events, you can modify the filter to store the authenticated user in a request scoped bean and then read it from your JAX-RS resource classes.
Compared to the approach that overrides the SecurityContext, the CDI approach allows you to get the authenticated user from beans other than JAX-RS resources and providers.
Supporting role-based authorization
Please refer to my other answer for details on how to support role-based authorization.
Issuing tokens
A token can be:
Opaque: Reveals no details other than the value itself (like a random string)
Self-contained: Contains details about the token itself (like JWT).
See details below:
Random string as token
A token can be issued by generating a random string and persisting it to a database along with the user identifier and an expiration date. A good example of how to generate a random string in Java can be seen here. You also could use:
Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);
JWT (JSON Web Token)
JWT (JSON Web Token) is a standard method for representing claims securely between two parties and is defined by the RFC 7519.
It's a self-contained token and it enables you to store details in claims. These claims are stored in the token payload which is a JSON encoded as Base64. Here are some claims registered in the RFC 7519 and what they mean (read the full RFC for further details):
iss: Principal that issued the token.
sub: Principal that is the subject of the JWT.
exp: Expiration date for the token.
nbf: Time on which the token will start to be accepted for processing.
iat: Time on which the token was issued.
jti: Unique identifier for the token.
Be aware that you must not store sensitive data, such as passwords, in the token.
The payload can be read by the client and the integrity of the token can be easily checked by verifying its signature on the server. The signature is what prevents the token from being tampered with.
You won't need to persist JWT tokens if you don't need to track them. Althought, by persisting the tokens, you will have the possibility of invalidating and revoking the access of them. To keep the track of JWT tokens, instead of persisting the whole token on the server, you could persist the token identifier (jti claim) along with some other details such as the user you issued the token for, the expiration date, etc.
When persisting tokens, always consider removing the old ones in order to prevent your database from growing indefinitely.
Using JWT
There are a few Java libraries to issue and validate JWT tokens such as:
jjwt
java-jwt
jose4j
To find some other great resources to work with JWT, have a look at http://jwt.io.
Handling token revocation with JWT
If you want to revoke tokens, you must keep the track of them. You don't need to store the whole token on server side, store only the token identifier (that must be unique) and some metadata if you need. For the token identifier you could use UUID.
The jti claim should be used to store the token identifier on the token. When validating the token, ensure that it has not been revoked by checking the value of the jti claim against the token identifiers you have on server side.
For security purposes, revoke all the tokens for a user when they change their password.
Additional information
It doesn't matter which type of authentication you decide to use. Always do it on the top of a HTTPS connection to prevent the man-in-the-middle attack.
Take a look at this question from Information Security for more information about tokens.
In this article you will find some useful information about token-based authentication.
This answer is all about authorization and it is a complement of my previous answer about authentication
Why another answer? I attempted to expand my previous answer by adding details on how to support JSR-250 annotations. However the original answer became the way too long and exceeded the maximum length of 30,000 characters. So I moved the whole authorization details to this answer, keeping the other answer focused on performing authentication and issuing tokens.
Supporting role-based authorization with the #Secured annotation
Besides authentication flow shown in the other answer, role-based authorization can be supported in the REST endpoints.
Create an enumeration and define the roles according to your needs:
public enum Role {
ROLE_1,
ROLE_2,
ROLE_3
}
Change the #Secured name binding annotation created before to support roles:
#NameBinding
#Retention(RUNTIME)
#Target({TYPE, METHOD})
public #interface Secured {
Role[] value() default {};
}
And then annotate the resource classes and methods with #Secured to perform the authorization. The method annotations will override the class annotations:
#Path("/example")
#Secured({Role.ROLE_1})
public class ExampleResource {
#GET
#Path("{id}")
#Produces(MediaType.APPLICATION_JSON)
public Response myMethod(#PathParam("id") Long id) {
// This method is not annotated with #Secured
// But it's declared within a class annotated with #Secured({Role.ROLE_1})
// So it only can be executed by the users who have the ROLE_1 role
...
}
#DELETE
#Path("{id}")
#Produces(MediaType.APPLICATION_JSON)
#Secured({Role.ROLE_1, Role.ROLE_2})
public Response myOtherMethod(#PathParam("id") Long id) {
// This method is annotated with #Secured({Role.ROLE_1, Role.ROLE_2})
// The method annotation overrides the class annotation
// So it only can be executed by the users who have the ROLE_1 or ROLE_2 roles
...
}
}
Create a filter with the AUTHORIZATION priority, which is executed after the AUTHENTICATION priority filter defined previously.
The ResourceInfo can be used to get the resource Method and resource Class that will handle the request and then extract the #Secured annotations from them:
#Secured
#Provider
#Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {
#Context
private ResourceInfo resourceInfo;
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Get the resource class which matches with the requested URL
// Extract the roles declared by it
Class<?> resourceClass = resourceInfo.getResourceClass();
List<Role> classRoles = extractRoles(resourceClass);
// Get the resource method which matches with the requested URL
// Extract the roles declared by it
Method resourceMethod = resourceInfo.getResourceMethod();
List<Role> methodRoles = extractRoles(resourceMethod);
try {
// Check if the user is allowed to execute the method
// The method annotations override the class annotations
if (methodRoles.isEmpty()) {
checkPermissions(classRoles);
} else {
checkPermissions(methodRoles);
}
} catch (Exception e) {
requestContext.abortWith(
Response.status(Response.Status.FORBIDDEN).build());
}
}
// Extract the roles from the annotated element
private List<Role> extractRoles(AnnotatedElement annotatedElement) {
if (annotatedElement == null) {
return new ArrayList<Role>();
} else {
Secured secured = annotatedElement.getAnnotation(Secured.class);
if (secured == null) {
return new ArrayList<Role>();
} else {
Role[] allowedRoles = secured.value();
return Arrays.asList(allowedRoles);
}
}
}
private void checkPermissions(List<Role> allowedRoles) throws Exception {
// Check if the user contains one of the allowed roles
// Throw an Exception if the user has not permission to execute the method
}
}
If the user has no permission to execute the operation, the request is aborted with a 403 (Forbidden).
To know the user who is performing the request, see my previous answer. You can get it from the SecurityContext (which should be already set in the ContainerRequestContext) or inject it using CDI, depending on the approach you go for.
If a #Secured annotation has no roles declared, you can assume all authenticated users can access that endpoint, disregarding the roles the users have.
Supporting role-based authorization with JSR-250 annotations
Alternatively to defining the roles in the #Secured annotation as shown above, you could consider JSR-250 annotations such as #RolesAllowed, #PermitAll and #DenyAll.
JAX-RS doesn't support such annotations out-of-the-box, but it could be achieved with a filter. Here are a few considerations to keep in mind if you want to support all of them:
#DenyAll on the method takes precedence over #RolesAllowed and #PermitAll on the class.
#RolesAllowed on the method takes precedence over #PermitAll on the class.
#PermitAll on the method takes precedence over #RolesAllowed on the class.
#DenyAll can't be attached to classes.
#RolesAllowed on the class takes precedence over #PermitAll on the class.
So an authorization filter that checks JSR-250 annotations could be like:
#Provider
#Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {
#Context
private ResourceInfo resourceInfo;
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
Method method = resourceInfo.getResourceMethod();
// #DenyAll on the method takes precedence over #RolesAllowed and #PermitAll
if (method.isAnnotationPresent(DenyAll.class)) {
refuseRequest();
}
// #RolesAllowed on the method takes precedence over #PermitAll
RolesAllowed rolesAllowed = method.getAnnotation(RolesAllowed.class);
if (rolesAllowed != null) {
performAuthorization(rolesAllowed.value(), requestContext);
return;
}
// #PermitAll on the method takes precedence over #RolesAllowed on the class
if (method.isAnnotationPresent(PermitAll.class)) {
// Do nothing
return;
}
// #DenyAll can't be attached to classes
// #RolesAllowed on the class takes precedence over #PermitAll on the class
rolesAllowed =
resourceInfo.getResourceClass().getAnnotation(RolesAllowed.class);
if (rolesAllowed != null) {
performAuthorization(rolesAllowed.value(), requestContext);
}
// #PermitAll on the class
if (resourceInfo.getResourceClass().isAnnotationPresent(PermitAll.class)) {
// Do nothing
return;
}
// Authentication is required for non-annotated methods
if (!isAuthenticated(requestContext)) {
refuseRequest();
}
}
/**
* Perform authorization based on roles.
*
* #param rolesAllowed
* #param requestContext
*/
private void performAuthorization(String[] rolesAllowed,
ContainerRequestContext requestContext) {
if (rolesAllowed.length > 0 && !isAuthenticated(requestContext)) {
refuseRequest();
}
for (final String role : rolesAllowed) {
if (requestContext.getSecurityContext().isUserInRole(role)) {
return;
}
}
refuseRequest();
}
/**
* Check if the user is authenticated.
*
* #param requestContext
* #return
*/
private boolean isAuthenticated(final ContainerRequestContext requestContext) {
// Return true if the user is authenticated or false otherwise
// An implementation could be like:
// return requestContext.getSecurityContext().getUserPrincipal() != null;
}
/**
* Refuse the request.
*/
private void refuseRequest() {
throw new AccessDeniedException(
"You don't have permissions to perform this action.");
}
}
Note: The above implementation is based on the Jersey RolesAllowedDynamicFeature. If you use Jersey, you don't need to write your own filter, just use the existing implementation.

Getting the user principal in Restlet over Tomcat

I have a web application deployed on Tomcat, which uses Tomcat's form authentication. When writing a new servlet, this allows me to find a request's user via HttpServletRequest#getUserPrincipal.
I would like to use Restlet in this app, and I was able to do so using Restlet's ServerServlet adaptor. However, it looks like I no longer have access to the user principal when receiving a new request in my resource classes. That is, the user prinicpal information is not carried through from Tomcat to Restlet.
Is there any way of obtaining the principal?
You should use the user principal with Restlet. As a matter of fact, Restlet has its own mechanism regarding security based on the challenge response. This allows to authenticate the user for a request, get its roles and set within ClientInfo#user. The servlet extension must be seen as an adapter to embed a Restlet engine within a servlet container but you shouldn't rely on the servlet API.
Here is the way to use security with Restlet:
public class MyApplication extends Application {
public Restlet createInboundRoot() {
Router router = new Router(getContext());
(...)
ChallengeAuthenticator ca = new ChallengeAuthenticator(getContext(),
ChallengeScheme.HTTP_BASIC, "admin");
Verifier verifier = (...)
Enroler enroler = new MyEnroler(this);
ca.setNext(router);
return ca;
}
}
Here is a sample implementation of Verifier:
public class MyVerifier extends SecretVerifier {
#Override
public boolean verify(String identifier, char[] secret) {
System.out.println(identifier);
System.out.println(secret);
//TODO compare with the Database
return true;
}
}
Here is a sample implementation of Enroler:
public class MyEnroler implements Enroler {
private Application application;
public MyEnroler(Application application) {
this.application = application;
}
public void enrole(ClientInfo clientInfo) {
Role role = new Role(application, "roleId",
"Role name");
clientInfo.getRoles().add(role);
}
}
You can then have access the security / authentication hints from the request within filter, server resource, ..., as described below:
User user = getRequest().getClientInfo().getUser();
List<Role> roles = getRequest().getClientInfo().getRoles();
You can notice this mechanism is opened in Restlet and can support a wide set of authentication (oauth2, ...). It's not really the good approach to use cookie-based authentication with REST. That said, you can use it even with Restlet.
Hope it helps you,
Thierry

How to correctly set the service URL in Spring's CAS service properties

When working with Spring Security + CAS I keep hitting a small road block with the callback URL that is sent to CAS, ie the service property. I've looked at a bunch of examples such as this and this but they all use hard coded URLs (even Spring's CAS docs). A typical snip looks something like this...
<bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
<property name="service" value="http://localhost:8080/click/j_spring_cas_security_check" />
</bean>
First, I don't want to hard code the server name or the port since I want this WAR to be deployable anywhere and I don't want my application tied to a particular DNS entry at compile time. Second, I don't understand why Spring can't auto detect my application's context and the request's URL to automagically build the URL. The first part of that statement still stand but As Raghuram pointed out below with this link, we can't trust the HTTP Host Header from the client for security reasons.
Ideally I would like service URL to be exactly what the user requested (as long as the request is valid such as a sub domain of mycompany.com) so it is seamless or at the very least I would like to only specify some path relative my applications context root and have Spring determine the service URL on the fly. Something like the following...
<bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
<property name="service" value="/my_cas_callback" />
</bean>
OR...
<bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
<property name="service" value="${container.and.app.derived.value.here}" />
</bean>
Is any of this possible or easy or have I missed the obvious?
I know this is a bit old but I just had to solve this very problem and couldn't really find anything in the newer stacks.
We have multiple environments sharing the same CAS service (think dev, qa, uat and local development environments); we have the ability to hit each environment from more than one url (via the client side web server over a reverse proxy and directly to the back-end server itself). This means that specifying a single url is difficult at best. Maybe there's a way to do this but being able to use a dynamic ServiceProperties.getService(). I'll probably add some kind of server suffix check to ensure that the url isn't hijacked at some point.
Here's what I did to get the basic CAS flow working regardless of the URL used to access the secured resource...
Override the CasAuthenticationFilter.
Override the CasAuthenticationProvider.
setAuthenticateAllArtifacts(true) on the ServiceProperties.
Here's the long form of my spring configuration bean:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
public class CasSecurityConfiguration extends WebSecurityConfigurerAdapter {
Just the usual spring configuration bean.
#Value("${cas.server.url:https://localhost:9443/cas}")
private String casServerUrl;
#Value("${cas.service.validation.uri:/webapi/j_spring_cas_security_check}")
private String casValidationUri;
#Value("${cas.provider.key:whatever_your_key}")
private String casProviderKey;
Some externalized configuration parameters.
#Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(casValidationUri);
serviceProperties.setSendRenew(false);
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}
The key thing above is the setAuthenticateAllArtifacts(true) call. This will make the service ticket validator use the AuthenticationDetailsSource implementation rather than a hard-coded ServiceProperties.getService() call
#Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
return new Cas20ServiceTicketValidator(casServerUrl);
}
Standard ticket validator..
#Resource
private UserDetailsService userDetailsService;
#Bean
public AuthenticationUserDetailsService authenticationUserDetailsService() {
return new AuthenticationUserDetailsService() {
#Override
public UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException {
String username = (token.getPrincipal() == null) ? "NONE_PROVIDED" : token.getName();
return userDetailsService.loadUserByUsername(username);
}
};
}
Standard hook to an existing UserDetailsService
#Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setAuthenticationUserDetailsService(authenticationUserDetailsService());
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
casAuthenticationProvider.setKey(casProviderKey);
return casAuthenticationProvider;
}
Standard authentication provider
#Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
casAuthenticationFilter.setServiceProperties(serviceProperties());
casAuthenticationFilter.setAuthenticationDetailsSource(dynamicServiceResolver());
return casAuthenticationFilter;
}
Key here is the dynamicServiceResolver() setting..
#Bean
AuthenticationDetailsSource<HttpServletRequest,
ServiceAuthenticationDetails> dynamicServiceResolver() {
return new AuthenticationDetailsSource<HttpServletRequest, ServiceAuthenticationDetails>() {
#Override
public ServiceAuthenticationDetails buildDetails(HttpServletRequest context) {
final String url = makeDynamicUrlFromRequest(serviceProperties());
return new ServiceAuthenticationDetails() {
#Override
public String getServiceUrl() {
return url;
}
};
}
};
}
Dynamically creates the service url from the makeDynamicUrlFromRequest() method. This bit is used upon ticket validation.
#Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint() {
#Override
protected String createServiceUrl(final HttpServletRequest request, final HttpServletResponse response) {
return CommonUtils.constructServiceUrl(null, response, makeDynamicUrlFromRequest(serviceProperties())
, null, serviceProperties().getArtifactParameter(), false);
}
};
casAuthenticationEntryPoint.setLoginUrl(casServerUrl + "/login");
casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
return casAuthenticationEntryPoint;
}
This part uses the same dynamic url creator when CAS wants to redirect to the login screen.
private String makeDynamicUrlFromRequest(ServiceProperties serviceProperties){
return "https://howeverYouBuildYourOwnDynamicUrl.com";
}
This is whatever you make of it. I only passed in the ServiceProperties to hold the URI of the service that we're configured for. We use HATEAOS on the back-side and have an implementation like:
return UriComponentsBuilder.fromHttpUrl(
linkTo(methodOn(ExposedRestResource.class)
.aMethodOnThatResource(null)).withSelfRel().getHref())
.replacePath(serviceProperties.getService())
.build(false)
.toUriString();
Edit: here's what I did for the list of valid server suffixes..
private List<String> validCasServerHostEndings;
#Value("${cas.valid.server.suffixes:company.com,localhost}")
private void setValidCasServerHostEndings(String endings){
validCasServerHostEndings = new ArrayList<>();
for (String ending : StringUtils.split(endings, ",")) {
if (StringUtils.isNotBlank(ending)){
validCasServerHostEndings.add(StringUtils.trim(ending));
}
}
}
private String makeDynamicUrlFromRequest(ServiceProperties serviceProperties){
UriComponents url = UriComponentsBuilder.fromHttpUrl(
linkTo(methodOn(ExposedRestResource.class)
.aMethodOnThatResource(null)).withSelfRel().getHref())
.replacePath(serviceProperties.getService())
.build(false);
boolean valid = false;
for (String validCasServerHostEnding : validCasServerHostEndings) {
if (url.getHost().endsWith(validCasServerHostEnding)){
valid = true;
break;
}
}
if (!valid){
throw new AccessDeniedException("The server is unable to authenticate the requested url.");
}
return url.toString();
}
In Spring 2.6.5 spring you could extend org.springframework.security.ui.cas.ServiceProperties
In spring 3 the method is final you could get around this by subclassing the CasAuthenticationProvider and CasEntryPoint and then use with your own version of ServiceProperties and override the getService() method with a more dynamic implementation.
You could use the host header to calculate the the required domain and make it more secure by validating that only domains/subdomains under your control are used. Then append to this some configurable value.
Of course you would be at risk that your implementation was insecure though... so be careful.
It could end up looking like:
<bean id="serviceProperties" class="my.ServiceProperties">
<property name="serviceRelativeUrl" value="/my_cas_callback" />
<property name="validDomainPattern" value="*.mydomain.com" />
</bean>
use maven, add a property placeholder, and configure it in your build process
I tried to subclass CasAuthenticationProvider as Pablojim suggest, but solution is very easier! with Spring Expression Language (SPEL) you can obtain the url dinamically.
Example: <property name="service"
value="https://#{T(java.net.InetAddress).getLocalHost().getHostName()}:${application.port}${cas.service}/login/cascheck"/>
I have not tried this myself, but it seems Spring Security has a solution to this with the SavedRequestAwareAuthenticationSuccessHandler shown in the update of Bob's blog.

Categories

Resources