Azure AD SSO in Java Web Application, ADFS SSO Configurations

Filed Under: Java

Azure AD SSO

The Single Sign-On feature is getting popular among developers to handle Application Access Management while developing multi-faced applications due to its remarkable advantages. Azure Active Directory is one of the most popular solutions used for Directory Management, Application Access Management and secured Identity Management, which offers the efficient Single Sign-On feature as well.

Azure AD SSO flow

Recently, I integrated Azure AD SSO with a Java web application along with synchronizing it with existing Identity Management system. I used Active Directory Federation Services ADFS 2016. In this blog, I am sharing the integration process in three sections.

  1. How to register Java Application in Azure AD
  2. How to implement ADAL Library in Java Application
  3. ADFS Configuration for Single Sign-On SSO

How to Register Java Application in Azure AD

Follow the instructions given in the link below to register the Java Application with Azure AD using Active Directory Authentication Library for Java (ADAL4J) and to acquire JWT access token.

https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-integrating-applications

How to implement ADAL library in Java Web Application

The process of implementing ADAL Library in Java Application is comprised of total six steps:

  1. Add ADAL Library Dependencies
  2. Register ADALFilter and add context parameters in your web.xml (Informing the Java App about Azure App configuration)
  3. Create AdalFilter
  4. Create the secure controller
  5. Create AuthHelper class
  6. Create aad.jsp

Add adal library dependencies to the java application


<dependency>
	<groupId>com.microsoft.azure</groupId>
	<artifactId>adal4j</artifactId>
	<version>1.1.1</version>
</dependency>
<dependency>
	<groupId>com.nimbusds</groupId>
	<artifactId>oauth2-oidc-sdk</artifactId>
	<version>4.5</version>
</dependency>

Register ADALFilter and add context parameters in your web.xml


<context-param>
	<param-name>authority</param-name>
	<param-value>https://login.windows.net/</param-value>
</context-param>
<context-param>
	<param-name>tenant</param-name>
	<param-value>YOUR_TENANT_NAME</param-value>
</context-param>

<filter>
	<filter-name>AdalFilter</filter-name>
	<filter-class>com.azilen.aad.AdalFilter</filter-class>
	<init-param>
		<param-name>client_id</param-name>
		<param-value><YOUR-CLIENT-ID></param-value>
	</init-param>
	<init-param>
		<param-name>secret_key</param-name>
		<param-value><YOUR-SECRET-KEY></param-value>
	</init-param>
</filter>
<filter-mapping>
	<filter-name> AdalFilter </filter-name>
	<url-pattern>/secure/*</url-pattern>
</filter-mapping>

Replace values of following with actual that you got from Azure portal while registering your application as described in the previous step.

YOUR-CLIENT-ID and YOUR-SECRET-KEY, authority (is your azure ad’s login page), YOUR_TENANT_NAME is the organization name.

Create AdalFilter

AdalFilter checks if current session has valid PRINCIPAL_SESSION_NAME stored, if not then it will redirect to Azure login page (Authority).


public class AdalFilter implements Filter {

	private String clientId = "";
	private String clientSecret = "";
	private String tenant = "";
	private String authority;

	public void destroy() {

	}

	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

		if (request instanceof HttpServletRequest) {
			HttpServletRequest httpRequest = (HttpServletRequest) request;
			HttpServletResponse httpResponse = (HttpServletResponse) response;
			try {

				String currentUri = request.getScheme() + "://" + request.getServerName()
						+ ("http".equals(request.getScheme()) && request.getServerPort() == 80
								|| "https".equals(request.getScheme()) && request.getServerPort() == 443 ? "" : ":" + request.getServerPort())
						+ httpRequest.getRequestURI();
				String fullUrl = currentUri + (httpRequest.getQueryString() != null ? "?" + httpRequest.getQueryString() : "");
				// check if user has a session
				if (!AuthHelper.isAuthenticated(httpRequest)) {
					if (AuthHelper.containsAuthenticationData(httpRequest)) {
						Map<String, String> params = new HashMap<String, String>();
						for (String key : request.getParameterMap().keySet()) {
							params.put(key, request.getParameterMap().get(key)[0]);
						}
						AuthenticationResponse authResponse = AuthenticationResponseParser.parse(new URI(fullUrl), params);
						if (AuthHelper.isAuthenticationSuccessful(authResponse)) {

							AuthenticationSuccessResponse oidcResponse = (AuthenticationSuccessResponse) authResponse;
							AuthenticationResult result = getAccessToken(oidcResponse.getAuthorizationCode(), currentUri);
							createSessionPrincipal(httpRequest, result);
						} else {
							AuthenticationErrorResponse oidcResponse = (AuthenticationErrorResponse) authResponse;
							throw new Exception(String.format("Request for auth code failed: %s - %s", oidcResponse.getErrorObject().getCode(),
									oidcResponse.getErrorObject().getDescription()));
						}
					} else {
						// not authenticated
						httpResponse.setStatus(302);
						httpResponse.sendRedirect(getRedirectUrl(currentUri));
						return;
					}
				} else {
					// if authenticated, how to check for valid session?
					AuthenticationResult result = AuthHelper.getAuthSessionObject(httpRequest);

					if (httpRequest.getParameter("refresh") != null) {
						result = getAccessTokenFromRefreshToken(result.getRefreshToken(), currentUri);
					} else {
						if (httpRequest.getParameter("cc") != null) {
							result = getAccessTokenFromClientCredentials();
						} else {
							if (result.getExpiresOnDate().before(new Date())) {
								result = getAccessTokenFromRefreshToken(result.getRefreshToken(), currentUri);
							}
						}
					}
					createSessionPrincipal(httpRequest, result);
				}
			} catch (Throwable exc) {
				httpResponse.setStatus(500);
				request.setAttribute("error", exc.getMessage());
				httpResponse.sendRedirect(((HttpServletRequest) request).getContextPath() + "/error.jsp");
			}
		}
		chain.doFilter(request, response);
	}

	private AuthenticationResult getAccessTokenFromClientCredentials() throws Throwable {
		AuthenticationContext context = null;
		AuthenticationResult result = null;
		ExecutorService service = null;
		try {
			service = Executors.newFixedThreadPool(1);
			context = new AuthenticationContext(authority + tenant + "/", true, service);
			Future<AuthenticationResult> future = context.acquireToken("https://graph.windows.net", new ClientCredential(clientId, clientSecret),
					null);
			result = future.get();
		} catch (ExecutionException e) {
			throw e.getCause();
		} finally {
			service.shutdown();
		}	

		if (result == null) {
			throw new ServiceUnavailableException("authentication result was null");
		}
		return result;
	}

	private AuthenticationResult getAccessTokenFromRefreshToken(String refreshToken, String currentUri) throws Throwable {
		AuthenticationContext context = null;
		AuthenticationResult result = null;
		ExecutorService service = null;
		try {
			service = Executors.newFixedThreadPool(1);
			context = new AuthenticationContext(authority + tenant + "/", true, service);
			Future<AuthenticationResult> future = context.acquireTokenByRefreshToken(refreshToken, new ClientCredential(clientId, clientSecret), null,
					null);
			result = future.get();
		} catch (ExecutionException e) {
			throw e.getCause();
		} finally {
			service.shutdown();
		}

		if (result == null) {
			throw new ServiceUnavailableException("authentication result was null");
		}
		return result;

	}

	private AuthenticationResult getAccessToken(AuthorizationCode authorizationCode, String currentUri) throws Throwable {
		String authCode = authorizationCode.getValue();
		ClientCredential credential = new ClientCredential(clientId, clientSecret);
		AuthenticationContext context = null;
		AuthenticationResult result = null;
		ExecutorService service = null;
		try {
			service = Executors.newFixedThreadPool(1);
			context = new AuthenticationContext(authority + tenant + "/", true, service);
			Future<AuthenticationResult> future = context.acquireTokenByAuthorizationCode(authCode, new URI(currentUri), credential, null);
			result = future.get();
		} catch (ExecutionException e) {
			throw e.getCause();
		} finally {
			service.shutdown();
		}

		if (result == null) {
			throw new ServiceUnavailableException("authentication result was null");
		}
		return result;
	}

	private void createSessionPrincipal(HttpServletRequest httpRequest, AuthenticationResult result) throws Exception {
		httpRequest.getSession().setAttribute(AuthHelper.PRINCIPAL_SESSION_NAME, result);
	}

	private String getRedirectUrl(String currentUri) throws UnsupportedEncodingException {
		String redirectUrl = authority + this.tenant
				+ "/oauth2/authorize?response_type=code%20id_token&scope=openid&response_mode=form_post&redirect_uri="
				+ URLEncoder.encode(currentUri, "UTF-8") + "&client_id=" + clientId + "&resource=https%3a%2f%2fgraph.windows.net" + "&nonce="
				+ UUID.randomUUID() + "&site_id=500879";
		return redirectUrl;
	}

	public void init(FilterConfig config) throws ServletException {
		clientId = config.getInitParameter("client_id");
		authority = config.getServletContext().getInitParameter("authority");
		tenant = config.getServletContext().getInitParameter("tenant");
		clientSecret = config.getInitParameter("secret_key");
	}

}

Create secure controller


@Controller
@RequestMapping("/secure/aad")
public class AadController { 
@RequestMapping(method = { RequestMethod.GET, RequestMethod.POST })
public String getDirectoryObjects(ModelMap model, HttpServletRequest httpRequest) {
	HttpSession session = httpRequest.getSession();
	log.info("session: " + session);
	AuthenticationResult result = (AuthenticationResult) session.getAttribute(AuthHelper.PRINCIPAL_SESSION_NAME);
	if (result == null) {
		model.addAttribute("error", new Exception("AuthenticationResult not found in session."));
		return "/error";
	} else {
		try {
			log.info("JWT token details:-");
			JWT jwt = JWTParser.parse(result.getIdToken());
			for (String key : jwt.getJWTClaimsSet().getAllClaims().keySet()) {
				log.info(key + ":" + jwt.getJWTClaimsSet().getAllClaims().get(key));
			}
			model.addAttribute("user", jwt.getJWTClaimsSet().getStringClaim("unique_name"));
		} catch (ParseException e) {
			log.error("Exception:", e);
		}
	}
	return "/secure/aad";
}
}

Create AuthHelper class


public final class AuthHelper {

	public static final String PRINCIPAL_SESSION_NAME = "principal";

	private AuthHelper() {
	}

	public static boolean isAuthenticated(HttpServletRequest request) {
		return request.getSession().getAttribute(PRINCIPAL_SESSION_NAME) != null;
	}

	public static AuthenticationResult getAuthSessionObject(HttpServletRequest request) {
		return (AuthenticationResult) request.getSession().getAttribute(PRINCIPAL_SESSION_NAME);
	}

	public static boolean containsAuthenticationData(HttpServletRequest httpRequest) {
		Map<String, String[]> map = httpRequest.getParameterMap();
		return (httpRequest.getMethod().equalsIgnoreCase("POST") 
					|| httpRequest.getMethod().equalsIgnoreCase("GET")) 
				&& (httpRequest.getParameterMap().containsKey(AuthParameterNames.ERROR)
						|| httpRequest.getParameterMap().containsKey(AuthParameterNames.ID_TOKEN)
						|| httpRequest.getParameterMap().containsKey(AuthParameterNames.CODE));
	}

	public static boolean isAuthenticationSuccessful(AuthenticationResponse authResponse) {
		return authResponse instanceof AuthenticationSuccessResponse;
	}
}

Create aad.jsp


<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>AAD Secure Page</title>
</head>
<body>

	<p>Welcome, ${user}</p>

	<ul>
		<li><a href="<%=request.getContextPath()%>/index.jsp">Go Home</a></li>
	</ul> 
</body>
</html>

Azure AD SSO Java Web Application Test

Below images shows the Azure AD login page and the success page after successful authentication.

Azure AD SSO Login Page

Azure AD SSO Login Successful page

You can download the complete project from our GitHub Repository.

ADFS Configuration for Single Sign-On (SSO)

ADFS SSO

To configure ADFS 2016, first please make sure that the application is running on SSL. The process of configuring ADFS to enable the synchronization between Azure AD and existing Identity Management system is comprised of three steps:

  1. Add Relying Party Trust
  2. Add client-id
  3. Perform the Authority command in PowerShell of ADFS server

Add Relying Party Trust

  • On Welcome page select Claims Aware
  • On Select Data Source select Enter data about the relying party manually
  • Provide any Display Name
  • Configure Certificate add self-signed certificate
  • Configure URL: Select either as per your ADFS protocol
    • Select the Enable support for the WS-Federation Passive protocol check box. Under Relying party WS-Federation Passive protocol URL, type the URL for this relying party trust.
    • Select the Enable support for the SAML 2.0 WebSSO protocol check box. Under Relying party 2.0 SSO service URL, type the Security Assertion Markup Language (SAML) service endpoint URL for this relying party trust.
  • Configure Identifiers: Add your application URL in Relying Party Trust Identifier.
  • Choose Access Control Policy: Select Permit All (Select as per your environment)
  • Ready to Add Trust: Review this setting
  • Finish.

Reference: https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/create-a-relying-party-trust

Add client-id with following command


Add-ADFSClient -Name "SampleApplication" -ClientId "<CLIENTID>" -RedirectUri @("REDIRECTURI") -Description "OAuth 2.0 client for our Test application"

Reference: https://docs.microsoft.com/en-us/powershell/module/adfs/add-adfsclient?view=win10-ps

Perform following Authority command in PowerShell of ADFS server


Grant-ADFSApplicationPermission -ClientRoleIdentifier "<CLIENTID>" -ServerRoleIdentifier "<AUTHORITY>" -ScopeNames "allatclaims","openid"
AUTHORITY eg: https://login.windows.net/

Conclusion

Regardless of the size and nature of any application, developers can deliver centralized policy-based access control to the application by utilizing Azure Active Directory’s standard platform. This integration and synchronization experience uplifted my knowledge of addressing business needs while simplifying the user journey across the system.

About Author
Vijay is a vigorous programmer who breathes technology for last five years. He is Sr. Software Engineer at Azilen Technologies harnessing his remarkable expertise over Spring, Hibernate, Liferay and JSF.

Comments

  1. Francis says:

    Hello. I have a java web app which am using shiro authentication and authorization. I need some help using microsoft AD and setting up user roles.. Any help will be greatly appreciated.

Leave a Reply

Your email address will not be published. Required fields are marked *

close
Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages