Skip to content

Instantly share code, notes, and snippets.

@Robinyo
Created January 9, 2022 20:37
Show Gist options
  • Save Robinyo/cc90c191be74ca173fb483199b3efceb to your computer and use it in GitHub Desktop.
Save Robinyo/cc90c191be74ca173fb483199b3efceb to your computer and use it in GitHub Desktop.
Private Key JWT Client Authentication
spring.profiles.active=@spring.profiles.active@
spring.main.banner-mode=off
# Logging
logging.level.root=INFO
logging.level.reactor.netty.http.client.HttpClient=INFO
package au.gov.dta.rp.controller;
import au.gov.dta.rp.model.TokenResponse;
import au.gov.dta.rp.service.AuthNService;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.SignedJWT;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.view.RedirectView;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
@AllArgsConstructor
@Controller
@RequestMapping("/")
@Slf4j
public class AuthNController {
private final AuthNService authNService;
@PostMapping("/sign-in")
public RedirectView signIn(HttpServletResponse response) {
log.info("AuthController POST /sign-in");
RedirectView redirectView = new RedirectView();
String state = UUID.randomUUID().toString();
Cookie authState = new Cookie("state", state);
authState.setMaxAge(3600);
response.addCookie(authState);
try {
redirectView.setUrl(authNService.getUrl(state));
} catch (Exception e) {
log.error("{}", e.getLocalizedMessage());
}
return redirectView;
}
@GetMapping("/authorization-code/callback")
public RedirectView authorizationCodeCallback(HttpServletRequest request,
@RequestParam(required = false) String code,
@RequestParam(required = false) String state,
@RequestParam(required = false) String error,
HttpServletResponse response,
RedirectAttributes redirectAttributes) {
log.info("AuthController GET /authorization-code/callback");
if (error != null) {
log.error(error);
redirectAttributes.addFlashAttribute("error_message", error);
return new RedirectView("/request-tokens-error-response", true);
}
log.info("code={}", code);
log.info("state={}", state);
Cookie[] cookies = request.getCookies();
String authState = "";
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase("state")) {
log.info("authState = cookie.getValue()");
authState = cookie.getValue();
}
}
if (!authState.equals(state)) {
String errorMessage = "'authState' is not equal to 'state'";
log.error(errorMessage);
redirectAttributes.addFlashAttribute("error_message", errorMessage);
return new RedirectView("/request-tokens-error-response", true);
}
String url = "/";
try {
TokenResponse tokenResponse = authNService.tokenRequest(code, state);
Cookie accessToken = new Cookie("access_token", tokenResponse.getAccess_token());
accessToken.setMaxAge(3600);
response.addCookie(accessToken);
JWT jwt = JWTParser.parse(tokenResponse.getId_token());
if (jwt instanceof SignedJWT) {
SignedJWT signedJWT = (SignedJWT) jwt;
JWTClaimsSet jwtClaimsSet = signedJWT.getJWTClaimsSet();
redirectAttributes.addFlashAttribute("given_name", jwtClaimsSet.getClaim("given_name"));
redirectAttributes.addFlashAttribute("family_name", jwtClaimsSet.getClaim("family_name"));
redirectAttributes.addFlashAttribute("birthdate", jwtClaimsSet.getClaim("birthdate"));
redirectAttributes.addFlashAttribute("iss", jwtClaimsSet.getClaim("iss"));
redirectAttributes.addFlashAttribute("acr", jwtClaimsSet.getClaim("acr"));
url = "/request-tokens-success-response";
} else {
url = "/request-tokens-error-response";
}
} catch (Exception e) {
url = "/request-tokens-error-response";
log.error("{}", e.getLocalizedMessage());
redirectAttributes.addFlashAttribute("error_message", e.getLocalizedMessage());
}
return new RedirectView(url, true);
}
}
package au.gov.dta.rp.service;
import au.gov.dta.rp.model.TokenResponse;
import au.gov.dta.rp.util.AuthConstants;
import au.gov.dta.rp.util.KeyStoreConstants;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.IOException;
import java.security.*;
import java.security.cert.CertificateException;
import java.util.Date;
import java.util.UUID;
import static org.springframework.web.reactive.function.BodyInserters.fromFormData;
@Service
@AllArgsConstructor
@Slf4j
public class AuthNService {
private final Environment env;
private final WebClient webClient;
public String getUrl(String state) {
log.info("AuthService -> getUrl()");
String clientId = env.getProperty("relying.party.client-id");
Assert.notNull(clientId, "clientId environment variable not found");
String nonce = UUID.randomUUID().toString();
UriComponents builder = UriComponentsBuilder
.fromHttpUrl(AuthConstants.BASE_URL)
.path(AuthConstants.SIGN_IN_PATH)
.queryParam("response_type", AuthConstants.RESPONSE_TYPE)
.queryParam("scope", AuthConstants.SCOPE)
.queryParam("state", state)
.queryParam("redirect_uri", AuthConstants.REDIRECT_URI)
.queryParam("nonce", nonce)
.queryParam("client_id", clientId)
.queryParam("acr_values", AuthConstants.ACR_VALUES)
.build()
.encode();
log.info("url = {}", builder.toString());
return builder.toString();
}
public TokenResponse tokenRequest(String code, String state) throws KeyStoreException, IOException, CertificateException,
NoSuchAlgorithmException, UnrecoverableKeyException, JOSEException {
log.info("AuthService -> tokenRequest()");
String clientId = env.getProperty("relying.party.client-id");
Assert.notNull(clientId, "clientId environment variable not found");
JWSSigner signer = null;
// Specify the key store type, e.g. Java KeyStore
KeyStore keyStore = KeyStore.getInstance(KeyStoreConstants.KEY_STORE_TYPE);
// You need a password to unlock the key store
char[] password = KeyStoreConstants.KEY_STORE_PASSWORD.toCharArray();
// Load the key store file (an unfiltered file in the /resources directory, see: pom.xml)
keyStore.load(new ClassPathResource(KeyStoreConstants.KEY_STORE_FILE).getInputStream(), password);
// Retrieve the private key
Key key = keyStore.getKey(KeyStoreConstants.KEY_STORE_ALIAS, password);
if (key instanceof PrivateKey) {
log.info("key instanceof PrivateKey");
signer = new RSASSASigner((PrivateKey) key);
}
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.issuer(clientId)
.subject(clientId)
.audience(AuthConstants.BASE_URL + AuthConstants.TOKEN_REQUEST_PATH)
.issueTime(new Date(new Date().getTime()))
.expirationTime(new Date(new Date().getTime() + AuthConstants.ONE_HOUR))
.jwtID(UUID.randomUUID().toString())
.build();
SignedJWT signedJWT = new SignedJWT(
new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(KeyStoreConstants.KEY_STORE_ALIAS).build(),
claimsSet);
signedJWT.sign(signer);
return webClient
.post()
.uri(uriBuilder -> uriBuilder
.path(AuthConstants.TOKEN_REQUEST_PATH)
.build())
.body(
fromFormData("grant_type", AuthConstants.GRANT_TYPE)
.with("code", code)
.with("client_id", clientId)
.with("redirect_uri", AuthConstants.REDIRECT_URI)
.with("client_assertion_type", AuthConstants.CLIENT_ASSERTION_TYPE)
.with("client_assertion", signedJWT.serialize()))
.retrieve()
.bodyToMono(TokenResponse.class)
.block();
}
}
package au.gov.dta.rp.util;
public class KeyStoreConstants {
public static final String KEY_STORE_ALIAS = "testkey";
public static final String KEY_STORE_FILE = "keystore.jks";
public static final String KEY_STORE_PASSWORD = "nomoresecrets";
public static final String KEY_STORE_TYPE = "JKS";
private KeyStoreConstants(){
throw new AssertionError();
}
}
/*
keytool -genkeypair \
-alias testkey \
-keyalg RSA \
-keysize 2048 \
-dname "CN=Rob Ferguson, OU=Digital Identity, O=DTA, L=Canberra, ST=ACT, C=AU" \
-keypass nomoresecrets \
-validity 100 \
-storetype JKS \
-keystore keystore.jks \
-storepass nomoresecrets
keytool -list \
-storetype JKS \
-keystore keystore.jks \
-storepass nomoresecrets
*/
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>au.gov.dta</groupId>
<artifactId>rp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Relying Party - Full Client with User Delegation</name>
<description>Use Java and Spring Boot to build an example Relying Party</description>
<inceptionYear>2021</inceptionYear>
<contributors>
<contributor>
<name>Rob Ferguson</name>
<roles>
<role>Software Engineer</role>
</roles>
<timezone>+10</timezone>
<url>https://robferguson/blog</url>
<email>rob.ferguson (at) robferguson.org</email>
</contributor>
</contributors>
<properties>
<java.version>11</java.version>
<nimbus-jose-jwt.version>9.7</nimbus-jose-jwt.version>
<bcpkix-jdk15on.version>1.68</bcpkix-jdk15on.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mustache</artifactId>
</dependency>
<!-- Adding both spring-boot-starter-web and spring-boot-starter-webflux modules in your application results in
Spring Boot auto-configuring Spring MVC, not WebFlux. -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbus-jose-jwt.version}</version>
</dependency>
<!--
Adds static JWKSet.load(KeyStore,PasswordLookup) method to load JWKs from a JCA key store.
Makes org.bouncycastle:bcpkix-jdk15on an optional dependency.
See: CHANGELOG.txt
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>${bcpkix-jdk15on.version}</version>
</dependency>
-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- Add Test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>dev</id>
<activation>
<!-- See: application.properties - spring.profiles.active=@spring.profiles.active@ -->
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<spring.profiles.active>dev</spring.profiles.active>
</properties>
</profile>
<profile>
<id>test</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<properties>
<spring.profiles.active>test</spring.profiles.active>
</properties>
</profile>
</profiles>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources/filtered</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<!-- Package as an executable jar -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>