I want to write REST service and I choose JWT for securing this rest service.
I declare 1 min for token, afterwards what I must do?
I must refresh token or something else?
If I must refresh token, user can call service's method with this token?
Token code
package com.example.demo.config;
import java.util.Date;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.ArrayList;
import java.util.Arrays;
public class TokenAuthenticationService {
//field of conf
static final long EXPIRATIONTIME = 60_000; // 1 min
static final String SECRET = "msg";
static final String TOKEN_PREFIX = "Bearer";
static final String HEADER_STRING = "Authorization";
//generate token
public static void addAuthentication(HttpServletResponse res, Authentication auth) {
String concattedRoles = "";
for (GrantedAuthority ga : auth.getAuthorities()) {
if (!"".equals(concattedRoles))
concattedRoles += "," + ga.getAuthority();
else
concattedRoles += ga.getAuthority();
}
String JWT = Jwts.builder().setSubject(auth.getName()).claim("roles", concattedRoles)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
.signWith(SignatureAlgorithm.HS512, SECRET).compact();
res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + JWT);
}
//get token from request header.
public static Authentication getAuthentication(HttpServletRequest request) {
try {
System.out.println("(Authentication getAuthentication(HttpServletRequest request)");
String token = request.getHeader(HEADER_STRING);
System.out.println("token=>"+token);
if (token != null) {
Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody();
String user = claims.getSubject();
String roles = (String) claims.get("roles");
if(claims.getExpiration().before(new Date(System.currentTimeMillis())))
throw new Exception(); //Here trow exception.
List<String> roleList = Arrays.asList(roles.split("\\s*,\\s*"));
List<GrantedAuthority> grantedAuths = new ArrayList<GrantedAuthority>();
for (int i = 0; i < roleList.size(); i++) {
System.out.println(roleList.get(i));
SimpleGrantedAuthority abv = new SimpleGrantedAuthority(roleList.get(i));
grantedAuths.add(abv);
}
System.out.println(grantedAuths);
return user != null ? new UsernamePasswordAuthenticationToken(user, null, grantedAuths) : null;
}
return null;
}catch (Exception e){
System.out.println(e);
return null;
}
}
}
How we implemented is -
First time user logs in we send them a token and a refresh token.
Client side then uses the 'token' in the header to make further API calls.
At client side, we maintain e countdown of 15 minutes (which is lesser than the expiry time of the main token), after which from the client side we send a request to the server with both token and refresh token.
After getting a valid refresh token along with the main token, the server sends back a new token with increased exipiry time.
Hope this helps.
Basically refresh_token is used for giving back a valid access_token to the user upon request. And refresh_tokens are usually long-lived rather than short-lived.
Personally, my design for securing a RESTful API is just to let them request the access_token to my endpoint i.e https://api.example.com/oauth/token every time, I don't provide a refresh_token because the idea for me is just to let them in into the resource, nothing else. And usually, the requesting resource will not be staying for so long on a particular session. For the other concerns of the server getting too many requests on the same user/session, you can implement a rate-limiting to your servers or token endpoint.
I based my API security implementations on PayPal and JHipster. They do not provide refresh_tokens to their respective RESTful API implementations, because in the end, refresh_tokens are optional to be used, and it's just a matter of what you want to achieve upon securing your RESTful endpoints.
For more information about refresh_token you can these links:
When to use JWT Tokens and Understanding refresh tokens.
Related
I am using Microsoft Graph SDK for some requests however everytime I perform a GET request it does another request to get a token. I've tried reading documentation about this but I cannot find anything in Java.
Here is my implementation of my client
ClientSecretCredential clientSecretCredential = new ClientSecretCredentialBuilder()
.clientId(clientId)
.clientSecret(clientSecret)
.tenantId(tenantId)
.httpClient(httpClient)
.build();
I have also tried using the method .tokenCachePersistenceOptions() in my builder but I get this warning/error
c.m.a.m.CrossProcessCacheFileLock : null
Thank you!
To achieve the above requirement Firstly you need to Authenticate for implementing MSAL to get the token from Azure AD.
To obtain an access token, your app must be registered with the Microsoft identity platform and approved to access the Microsoft Graph resources it requires by either a user who is added as an owner for that application or an administrator.
For complete setup please refer this MS DOCUMENT Authentication and authorization basics for Microsoft Graph , This sample & GitHub sample|msgraph-sdk-java-core
I was looking for the same think and here's what I've implemented for it:
Implement your own authentication provider class:
public class DelegateAuthenticationProvider implements IAuthenticationProvider {
private String token;
public DelegateAuthenticationProvider(String token) {
this.token = token;
}
#NotNull
#Override
public CompletableFuture<String> getAuthorizationTokenAsync(#NotNull URL url) {
return CompletableFuture.completedFuture(token);
}
}
Then you can use it as follow:
String token = "<YOUR_TOKEN_STRING>"
IAuthenticationProvider tokenCredentialAuthProvider = new DelegateAuthenticationProvider(token);
// Create the Graph Client with the given Token Provider
GraphServiceClient graphClient = GraphServiceClient.builder()
.authenticationProvider(tokenCredentialAuthProvider)
.buildClient();
If you get an GraphServiceException code 401 you should renew your token.
When you are successfully logged in with your clientSecretCredential, here's how you can get the token:
List<String> scopes = Arrays.asList("https://graph.microsoft.com/.default");
IAuthenticationProvider tokenCredentialAuthProvider = new TokenCredentialAuthProvider(scopes, clientSecretCredential);
String token = tokenCredentialAuthProvider.getAuthorizationTokenAsync("https://graph.microsoft.com/v1.0/me").get()
Hope this helps.
You can override the authenticationProvider which is provided for the GraphServiceClient
import com.azure.core.credential.AccessToken;
import com.azure.core.credential.TokenCredential;
import com.azure.core.credential.TokenRequestContext;
import com.microsoft.graph.authentication.TokenCredentialAuthProvider;
import org.jetbrains.annotations.NotNull;
import java.net.URL;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
public class CachingTokenCredentialAuthProvider extends TokenCredentialAuthProvider {
private final TokenCredential tokenCredential;
private final TokenRequestContext context;
private AccessToken accessToken;
public CachingTokenCredentialAuthProvider(#NotNull List<String> scopes, #NotNull TokenCredential tokenCredential) {
super(scopes, tokenCredential);
if (!scopes.isEmpty()) {
this.context = new TokenRequestContext();
this.context.setScopes(scopes);
this.tokenCredential = Objects.requireNonNull(tokenCredential, "tokenCredential parameter cannot be null.");
} else {
throw new IllegalArgumentException("scopes parameter cannot be null or empty");
}
}
#NotNull
#Override
public CompletableFuture<String> getAuthorizationTokenAsync(#NotNull URL requestUrl) {
if (this.shouldAuthenticateRequestWithUrl(Objects.requireNonNull(requestUrl, "requestUrl parameter cannot be null"))) {
if(this.accessToken != null && !OffsetDateTime.now().minusMinutes(1).isAfter(this.accessToken.getExpiresAt())) {
return CompletableFuture.completedFuture(this.accessToken.getToken());
}
return this.tokenCredential.getToken(this.context).toFuture().thenApply(accessToken -> {
saveToken(accessToken);
return accessToken.getToken();
});
} else {
return CompletableFuture.completedFuture(null);
}
}
void saveToken(AccessToken accessToken) {
this.accessToken = accessToken;
}
}
This will cache the token until it one minute before it is no longer valid.
From this artcle, I have implemented calling another rest API from my REST API method in micronaut gradle application. Since my REST API expects jwt token I am sending the same token I received with in current request. I am seeing Unauthorized error even token is being passed. Can anyone help in this regard. Below is my code.
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.appter.clientmgmt.models.ClientContact;
import io.appter.clientmgmt.repositories.IClientContactRepository;
import io.micronaut.http.uri.UriTemplate;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.rules.SecurityRule;
import io.micronaut.http.annotation.*;
import io.micronaut.http.client.RxHttpClient;
import io.micronaut.http.client.annotation.Client;
import io.reactivex.Flowable;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import javax.validation.constraints.NotNull;
import java.security.Security;
import java.util.List;
#Controller("/clientcontact")
//#Secured(SecurityRule.IS_ANONYMOUS)
public class ClientContactController {
private static final Logger LOG = LoggerFactory.getLogger(ClientContactController.class);
private IClientContactRepository clientContactRepository;
private final RxHttpClient httpClient;
public ClientContactController(IClientContactRepository clientContactRepository,
#Client("http://appterauthsvc-env.g2yapp2kcp.us-east-1.elasticbeanstalk.com") RxHttpClient httpClient) {
this.clientContactRepository = clientContactRepository;
this.httpClient = httpClient;
}
#Get("/")
public HttpStatus index() {
return HttpStatus.OK;
}
#Post("/")
#Secured(SecurityRule.IS_AUTHENTICATED)
public ClientContact createClientContact(#Body ClientContact clientContact,
Authentication authentication,
#Header("Authorization") String authorization) {
try {
List<ClientContact> existingClientContacts = clientContactRepository.getClientContactByClientId(clientContact.getClientId());
LOG.info("current contacts count for the client " + clientContact.getClientId() + " is " + existingClientContacts.size());
if (existingClientContacts.isEmpty()) {
User userObj = new User();
Long clientId = new Long(clientContact.getClientId());
userObj.setClientId(clientId);
userObj.setFirstName(clientContact.getFirstName());
userObj.setLastName(clientContact.getLastName());
userObj.setEmailId(clientContact.getEmailAddress());
userObj.setPhoneNo(clientContact.getContactNumber());
userObj.setIsActive(true);
LOG.info("User Email set is: "+userObj.getEmailId());
LOG.info("authorization token is: "+authorization);
HttpRequest<?> request = HttpRequest.POST("/user", userObj).bearerAuth(authorization);
String response = httpClient.toBlocking().retrieve(request);
LOG.info("Request Object: "+ request.toString());
LOG.info("Response Object: "+ response.toString());
LOG.info("User API executed.. ");
}
return clientContactRepository.createClientContact(clientContact);
} catch (Exception ex) {
LOG.error(ex.getMessage(), ex);
return null;
}
}
}
Thanks in advance.
Likely because #Header("Authorization") String authorization is returning something like Bearer xyz... and the bearerAuth method is adding Bearer to the string so you are sending Bearer Bearer xyz...
So just do .header(HttpHeaders.AUTHORIZATION, authorization)
Also as a side note you really shouldn't be doing blocking HTTP calls in this method. It's not the end of the world since in this case you're blocking an IO thread, however this type of code should be avoided.
I'm looking for refresh token by using the Java Admin Client https://github.com/keycloak/keycloak/tree/master/integration/admin-client
Cannot find anything about it
Regards
EDIT :
finally I go that :
public AccessTokenResponse executeRefresh(String refreshToken) {
String url = "https://url/auth" + "/realms/" + keycloakRealm + "/protocol/openid-connect/token";
Configuration kcConfig = new Configuration(authServerUrl, keycloakRealm, keycloakInternalClientId, null, null);
Http http = new Http(kcConfig, (params, headers) -> {
});
return http.<AccessTokenResponse>post(url)
.authentication()
.client()
.form()
.param("grant_type", "refresh_token")
.param("refresh_token", refreshToken)
.param("client_id", keycloakInternalClientId)
.param("client_secret", keycloakInternalClientSecret)
.response()
.json(AccessTokenResponse.class)
.execute();
}
org.keycloak.admin.client.Keycloak.tokenManager().refreshToken() can refresh token. For example:
// Create a Keycloak client
Keycloak kc = KeycloakBuilder.builder()
.serverUrl("http://localhost:8080/auth")
.realm("master")
.username("admin")
.password("password")
.clientId("admin-cli")
.resteasyClient(new ResteasyClientBuilder().connectionPoolSize(10).build())
.build();
TokenManager tokenManager = kc.tokenManager();
// get access token
AccessTokenResponse accessTokenResponse = tokenManager.getAccessToken();
System.out.println(accessTokenResponse.getRefreshToken());
// Refresh token
accessTokenResponse = tokenManager.refreshToken();
System.out.println(accessTokenResponse.getRefreshToken());
Sadly Java Admin Client does not have this kind of functionality (hopefully will have in the future)
For now, look at this answer:
Refresh access_token via refresh_token in Keycloak
Have achieved it through RestTemplate inside my spring-boot application.
Below is the code I have used to get refresh token :-
public ResponseEntity<RefreshTokenResponse> refreshToken(String refreshToken) {
String url = authUrl+ "/realms/" + realm + "/protocol/openid-connect/token";
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("grant_type", "refresh_token");
map.add("refresh_token", refreshToken);
map.add("client_id", clientId);
map.add("client_secret", clientSecret);
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers);
ResponseEntity response =
restTemplate.exchange(url,
HttpMethod.POST,
entity,
Object.class);
return response;}
RefreshTokenResponse :-
#Data
public class RefreshTokenResponse {
private AccessTokenResponse token;
}
Keycloak v19.0.0
authUrl :- where keycloak server is running. my case
http://localhost:8080/
ream :- your realm name
Actually, I managed to make Keycloak client do this job for me after spending some time with the issue. In my case I had to connect to a Keycloak server with password grant type, and use access token to fetch data from a third party protected endpoint in a Spring Boot server side application.
At the end I came up with a service, which provides an access token after initial authentication, and automatic refresh/re-authentication on demand.
I added a #Configuration bean, which contained the connection parameters to the third party Keycloak instance:
package no.currentclient.application.api.config; // real package name masked
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
public class OauthClientConfig {
public record OauthConfig(String realm, String authServerUrl, String clientId, String username, String password) {
}
#Bean
OauthConfig oauthConfig(
#Value("${client.oauth.realm}") String realm,
#Value("${client.oauth.auth-server-url}") String authServerUrl,
#Value("${client.oauth.resource}") String clientId,
#Value("${client.oauth.username}") String username,
#Value("${client.oauth.password}") String password
) {
return new OauthConfig(realm,
authServerUrl,
clientId,
username,
password);
}
}
After I created a Spring Service which is capable of authenticating, getting and refreshing an access token:
package no.currentclient.application.auth.oauthclient; // real package name masked
import com.fasterxml.jackson.databind.ObjectMapper;
import no.currentclient.application.api.config.OauthClientConfig;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.authorization.client.util.Http;
import org.keycloak.authorization.client.util.TokenCallable;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Collections;
#Service
public class OauthTokenService {
private final TokenCallable tokenCallable;
public OauthTokenService(
OauthClientConfig.OauthConfig oauthConfig,
OkHttpClient okHttpClient
) throws IOException {
var serverConfiguration = getServerConfiguration(oauthConfig.authServerUrl()+"/auth/realms/"+oauthConfig.realm()+"/.well-known/openid-configuration", okHttpClient);
var config = new org.keycloak.authorization.client.Configuration(
// These might all be set to null -> only tokenMinimumTimeToLive is used in TokenCallable...
null,null,null, null,null);
var http = new Http(config, (requestParams, requestHeaders) -> requestParams.put("client_id", Collections.singletonList("deichman")));
tokenCallable = new TokenCallable(oauthConfig.username(), oauthConfig.password(), http, config, serverConfiguration);
}
/*
* Call this method to get hold of an on-demand refreshed auth token. TokenCallable handles the burden of token
* refresh and re-authentication in case of session timeout.
*/
public String getAccessToken() {
return tokenCallable.call();
}
private ServerConfiguration getServerConfiguration(String configUrl, OkHttpClient okHttpClient) throws IOException {
var configRequest = new Request.Builder().url(configUrl).get().build();
try (var response = okHttpClient.newCall(configRequest).execute()) {
return new ObjectMapper().readValue(response.body().string(), ServerConfiguration.class);
}
}
}
TokenCallable hides all the complexity of refresh/re-authentication on demand.
Hope it helps a few struggling with this problem.
When I get a token by authorization code (authContext.acquireTokenByAuthorizationCode), I get a JWT (idToken) that is signed and has the proper headers:
{
"typ": "JWT",
"alg": "RS256",
"x5t": "wLLmYfsqdQuWtV_-hnVtDJJZM3Q",
"kid": "wLLmYfsqdQuWtV_-hnVtDJJZM3Q"
}
but when I use the refresh token to get a new token (authContext.acquireTokenByRefreshToken(...)), it returns an unsigned JWT:
{
"typ": "JWT",
"alg": "none"
}
How do I get it to give me a signed JWT?
return authContext.acquireTokenByRefreshToken(
refreshToken,
new ClientCredentials(
clientId,
clientSecret
),
null
);
I did not reproduce your issue on my side. I followed this tutorial to get Authentication code and acquire access token and refresh token with below code successfully. Please refer to it.
import com.microsoft.aad.adal4j.AuthenticationContext;
import com.microsoft.aad.adal4j.AuthenticationResult;
import com.microsoft.aad.adal4j.ClientCredential;
import java.net.URI;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class GetTokenByAuthenticationCode {
private static final String APP_ID = "***";
private static final String APP_SECRET = "***";
private static final String REDIRECT_URI = "http://localhost:8080";
private static final String tenant = "***";
public static void main(String[] args) throws Exception {
String authority = "https://login.microsoftonline.com/" + tenant + "/oauth2/authorize";
ExecutorService service = Executors.newFixedThreadPool(1);
String code = "***";
AuthenticationContext context = new AuthenticationContext(authority, true, service);
URI url = new URI(REDIRECT_URI);
Future<AuthenticationResult> result = context.acquireTokenByAuthorizationCode(
code,
url,
new ClientCredential(APP_ID, APP_SECRET),
null
);
String token = result.get().getAccessToken();
System.out.println(token);
String refreshToken = result.get().getRefreshToken();
System.out.println(refreshToken);
Future<AuthenticationResult> result1 = context.acquireTokenByRefreshToken(
refreshToken,
new ClientCredential(APP_ID, APP_SECRET),
null
);
String tokenNew = result1.get().getAccessToken();
String refreshTokenNew = result1.get().getRefreshToken();
System.out.println(tokenNew);
System.out.println(refreshTokenNew);
}
}
Decode:
Update Answer:
Firstly, sorry for the mistake. I replaced getIdToken with getAccessToken, the result is as same as you.Then I searched the response parameters in Authorize access to Azure Active Directory web applications using the OAuth 2.0 code grant flow, you could find the statement of id_token parameter.
An unsigned JSON Web Token (JWT) representing an ID token. The app can
base64Url decode the segments of this token to request information
about the user who signed in. The app can cache the values and display
them, but it should not rely on them for any authorization or security
boundaries.
So, the id token just a segment which can't be relied on. If you want to get the complete id token, please refer to the openId flow.
Has anyone successfully configured Spring Boot OAuth2 with ADFS as the identity provider? I followed this tutorial successfully for Facebook, https://spring.io/guides/tutorials/spring-boot-oauth2/, but ADFS doesn't appear to have a userInfoUri. I think ADFS returns the claims data in the token itself (JWT format?), but not sure how to make that work with Spring. Here is what I have so far in my properties file:
security:
oauth2:
client:
clientId: [client id setup with ADFS]
userAuthorizationUri: https://[adfs domain]/adfs/oauth2/authorize?resource=[MyRelyingPartyTrust]
accessTokenUri: https://[adfs domain]/adfs/oauth2/token
tokenName: code
authenticationScheme: query
clientAuthenticationScheme: form
grant-type: authorization_code
resource:
userInfoUri: [not sure what to put here?]
tldr; ADFS embeds user information in the oauth token. You need to create and override the org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices object to extract this information and add it to the Principal object
To get started, first follow the Spring OAuth2 tutorial: https://spring.io/guides/tutorials/spring-boot-oauth2/. Use these application properties (fill in your own domain):
security:
oauth2:
client:
clientId: [client id setup with ADFS]
userAuthorizationUri: https://[adfs domain]/adfs/oauth2/authorize?resource=[MyRelyingPartyTrust]
accessTokenUri: https://[adfs domain]/adfs/oauth2/token
tokenName: code
authenticationScheme: query
clientAuthenticationScheme: form
grant-type: authorization_code
resource:
userInfoUri: https://[adfs domain]/adfs/oauth2/token
Note: We will be ignoring whatever is in the userInfoUri, but Spring OAuth2 seems to require something be there.
Create a new class, AdfsUserInfoTokenServices, which you can copy and tweak below (you will want to clean it up some). This is a copy of the Spring class; You could probably extend it if you want, but I made enough changes where that didn't seem like it gained me much:
package edu.bowdoin.oath2sample;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor;
import org.springframework.boot.autoconfigure.security.oauth2.resource.FixedAuthoritiesExtractor;
import org.springframework.boot.autoconfigure.security.oauth2.resource.FixedPrincipalExtractor;
import org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.util.Assert;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
public class AdfsUserInfoTokenServices implements ResourceServerTokenServices {
protected final Logger logger = LoggerFactory.getLogger(getClass());
private final String userInfoEndpointUrl;
private final String clientId;
private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE;
private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor();
private PrincipalExtractor principalExtractor = new FixedPrincipalExtractor();
public AdfsUserInfoTokenServices(String userInfoEndpointUrl, String clientId) {
this.userInfoEndpointUrl = userInfoEndpointUrl;
this.clientId = clientId;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public void setRestTemplate(OAuth2RestOperations restTemplate) {
// not used
}
public void setAuthoritiesExtractor(AuthoritiesExtractor authoritiesExtractor) {
Assert.notNull(authoritiesExtractor, "AuthoritiesExtractor must not be null");
this.authoritiesExtractor = authoritiesExtractor;
}
public void setPrincipalExtractor(PrincipalExtractor principalExtractor) {
Assert.notNull(principalExtractor, "PrincipalExtractor must not be null");
this.principalExtractor = principalExtractor;
}
#Override
public OAuth2Authentication loadAuthentication(String accessToken)
throws AuthenticationException, InvalidTokenException {
Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
if (map.containsKey("error")) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("userinfo returned error: " + map.get("error"));
}
throw new InvalidTokenException(accessToken);
}
return extractAuthentication(map);
}
private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
Object principal = getPrincipal(map);
List<GrantedAuthority> authorities = this.authoritiesExtractor
.extractAuthorities(map);
OAuth2Request request = new OAuth2Request(null, this.clientId, null, true, null,
null, null, null, null);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
principal, "N/A", authorities);
token.setDetails(map);
return new OAuth2Authentication(request, token);
}
/**
* Return the principal that should be used for the token. The default implementation
* delegates to the {#link PrincipalExtractor}.
* #param map the source map
* #return the principal or {#literal "unknown"}
*/
protected Object getPrincipal(Map<String, Object> map) {
Object principal = this.principalExtractor.extractPrincipal(map);
return (principal == null ? "unknown" : principal);
}
#Override
public OAuth2AccessToken readAccessToken(String accessToken) {
throw new UnsupportedOperationException("Not supported: read access token");
}
private Map<String, Object> getMap(String path, String accessToken) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Getting user info from: " + path);
}
try {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(
accessToken);
token.setTokenType(this.tokenType);
logger.debug("Token value: " + token.getValue());
String jwtBase64 = token.getValue().split("\\.")[1];
logger.debug("Token: Encoded JWT: " + jwtBase64);
logger.debug("Decode: " + Base64.getDecoder().decode(jwtBase64.getBytes()));
String jwtJson = new String(Base64.getDecoder().decode(jwtBase64.getBytes()));
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(jwtJson, new TypeReference<Map<String, Object>>(){});
}
catch (Exception ex) {
this.logger.warn("Could not fetch user details: " + ex.getClass() + ", "
+ ex.getMessage());
return Collections.<String, Object>singletonMap("error",
"Could not fetch user details");
}
}
}
The getMap method is where the token value is parsed and the JWT formatted user info is extracted and decoded (error checking can be improved here, this is a rough draft, but gives you the gist). See toward the bottom of this link for information on how ADFS embeds data in the token: https://blogs.technet.microsoft.com/askpfeplat/2014/11/02/adfs-deep-dive-comparing-ws-fed-saml-and-oauth/
Add this to your configuration:
#Autowired
private ResourceServerProperties sso;
#Bean
public ResourceServerTokenServices userInfoTokenServices() {
return new AdfsUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId());
}
Now follow the first part of these instructions to setup an ADFS client and a relying party trust: https://vcsjones.com/2015/05/04/authenticating-asp-net-5-to-ad-fs-oauth/
You need to add the id of your relying party trust to the properties file userAuthorizationUri as the value of the parameter 'resource'.
Claim Rules:
If you don't want to have to create your own PrincipalExtractor or AuthoritiesExtractor (see the AdfsUserInfoTokenServices code), set whatever attribute you are using for the username (e.g. SAM-Account-Name) so that it has and Outgoing Claim Type 'username'. When creating claim rules for groups, make sure the Claim type is "authorities" (ADFS just let me type that in, there isn't an existing claim type by that name). Otherwise, you can write extractors to work with the ADFS claim types.
Once that is all done, you should have a working example. There are a lot of details here, but once you get it down, it's not too bad (easier than getting SAML to work with ADFS). The key is understanding the way ADFS embeds data in the OAuth2 token and understanding how to use the UserInfoTokenServices object. Hope this helps someone else.
Although this question is old, there is no other reference on the web on how to integrate Spring OAuth2 with ADFS.
I therefore added a sample project on how to integrate with Microsoft ADFS using the out of the box spring boot auto-configuration for Oauth2 Client:
https://github.com/selvinsource/spring-security/tree/oauth2login-adfs-sample/samples/boot/oauth2login#adfs-login
Additionally to the accepted answer:
#Ashika wants to know if you can use this with REST instead of form login.
Just switch from #EnableOAuth2Sso to #EnableResourceServer annotation.
With the #EnableResourceServer annotation you keep the cabability to use SSO although you didn't use the #EnableOAuth2Sso annotation. Your running as a resource server.
https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/htmlsingle/#boot-features-security-oauth2-resource-server
#Erik, This is a very good explanation of how to get things going in terms of using ADFS as both identity and authorization provider. There was on thing I stumbled on was to get "upn" and "email" information in the JWT token. This is the decoded JWT information I received -
2017-07-13 19:43:15.548 INFO 3848 --- [nio-8080-exec-7] c.e.demo.AdfsUserInfoTokenServices : Decoded JWT: {"aud":"http://localhost:8080/web-app","iss":"http://adfs1.example.com/adfs/services/trust","iat":1500000192,"exp":1500003792,"apptype":"Confidential","appid":"1fd9b444-8ba4-4d82-942e-91aaf79f5fd0","authmethod":"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport","auth_time":"2017-07-14T02:43:12.570Z","ver":"1.0"}
But post adding both email-id and upn in under the "Issuance Transform Rules" and adding "Send LDAP Attributes as Claims" claims rule to send User-Principal-Name as user_id (on of the PRINCIPAL_KETS that FixedPrincipalExtractor - Spring security) I was able to record the user_id being used to login on my UI application. Here is the decoded JWT post adding the claims rule -
2017-07-13 20:16:05.918 INFO 8048 --- [nio-8080-exec-3] c.e.demo.AdfsUserInfoTokenServices : Decoded JWT: {"aud":"http://localhost:8080/web-app","iss":"http://adfs1.example.com/adfs/services/trust","iat":1500002164,"exp":1500005764,"upn":"sample.user1#example.com","apptype":"Confidential","appid":"1fd9b444-8ba4-4d82-942e-91aaf79f5fd0","authmethod":"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport","auth_time":"2017-07-14T03:16:04.745Z","ver":"1.0"}