Skip to content

Commit 567520b

Browse files
committed
SDK-2771: Add new maven module for supporting the creation of Yoti authentication tokens via API
1 parent f224ab0 commit 567520b

9 files changed

Lines changed: 644 additions & 0 deletions

File tree

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<modules>
1414
<module>yoti-sdk-parent</module>
1515
<module>yoti-sdk-api</module>
16+
<module>yoti-sdk-auth</module>
1617
<module>yoti-sdk-sandbox</module>
1718
<module>yoti-sdk-spring-boot-auto-config</module>
1819
<module>yoti-sdk-spring-security</module>

yoti-sdk-auth/pom.xml

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>com.yoti</groupId>
9+
<artifactId>yoti-sdk-parent</artifactId>
10+
<version>4.0.0-SNAPSHOT</version>
11+
<relativePath>../yoti-sdk-parent</relativePath>
12+
</parent>
13+
14+
<artifactId>yoti-sdk-auth</artifactId>
15+
16+
<properties>
17+
<jjwt.version>0.13.0</jjwt.version>
18+
</properties>
19+
20+
<dependencies>
21+
<dependency>
22+
<groupId>org.bouncycastle</groupId>
23+
<artifactId>bcpkix-jdk18on</artifactId>
24+
</dependency>
25+
<dependency>
26+
<groupId>com.yoti</groupId>
27+
<artifactId>yoti-sdk-api</artifactId>
28+
</dependency>
29+
<dependency>
30+
<groupId>io.jsonwebtoken</groupId>
31+
<artifactId>jjwt-api</artifactId>
32+
<version>${jjwt.version}</version>
33+
</dependency>
34+
<dependency>
35+
<groupId>com.fasterxml.jackson.core</groupId>
36+
<artifactId>jackson-databind</artifactId>
37+
</dependency>
38+
<dependency>
39+
<groupId>commons-logging</groupId>
40+
<artifactId>commons-logging</artifactId>
41+
<version>1.1.1</version>
42+
</dependency>
43+
<dependency>
44+
<groupId>io.jsonwebtoken</groupId>
45+
<artifactId>jjwt-impl</artifactId>
46+
<version>${jjwt.version}</version>
47+
<scope>runtime</scope>
48+
</dependency>
49+
<dependency>
50+
<groupId>io.jsonwebtoken</groupId>
51+
<artifactId>jjwt-jackson</artifactId>
52+
<version>${jjwt.version}</version>
53+
<scope>runtime</scope>
54+
</dependency>
55+
56+
<!-- Testing dependencies -->
57+
<dependency>
58+
<groupId>org.hamcrest</groupId>
59+
<artifactId>hamcrest-library</artifactId>
60+
<scope>test</scope>
61+
</dependency>
62+
<dependency>
63+
<groupId>junit</groupId>
64+
<artifactId>junit</artifactId>
65+
<scope>test</scope>
66+
</dependency>
67+
<dependency>
68+
<groupId>org.mockito</groupId>
69+
<artifactId>mockito-core</artifactId>
70+
<scope>test</scope>
71+
</dependency>
72+
73+
</dependencies>
74+
75+
<build>
76+
<plugins>
77+
<plugin>
78+
<groupId>org.owasp</groupId>
79+
<artifactId>dependency-check-maven</artifactId>
80+
</plugin>
81+
<plugin>
82+
<groupId>org.codehaus.mojo</groupId>
83+
<artifactId>animal-sniffer-maven-plugin</artifactId>
84+
</plugin>
85+
<plugin>
86+
<artifactId>maven-enforcer-plugin</artifactId>
87+
</plugin>
88+
<plugin>
89+
<artifactId>maven-compiler-plugin</artifactId>
90+
<version>${maven-compiler-plugin.version}</version>
91+
</plugin>
92+
<plugin>
93+
<artifactId>maven-source-plugin</artifactId>
94+
</plugin>
95+
<plugin>
96+
<artifactId>maven-javadoc-plugin</artifactId>
97+
</plugin>
98+
<plugin>
99+
<groupId>org.jacoco</groupId>
100+
<artifactId>jacoco-maven-plugin</artifactId>
101+
</plugin>
102+
</plugins>
103+
</build>
104+
105+
<reporting>
106+
<plugins>
107+
<plugin>
108+
<artifactId>maven-project-info-reports-plugin</artifactId>
109+
</plugin>
110+
</plugins>
111+
</reporting>
112+
113+
</project>
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package com.yoti.auth;
2+
3+
import static com.yoti.validation.Validation.notNull;
4+
import static com.yoti.validation.Validation.notNullOrEmpty;
5+
6+
import java.io.IOException;
7+
import java.net.MalformedURLException;
8+
import java.net.URL;
9+
import java.security.KeyPair;
10+
import java.time.OffsetDateTime;
11+
import java.util.Date;
12+
import java.util.HashMap;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.UUID;
16+
import java.util.function.Supplier;
17+
18+
import com.yoti.api.client.InitialisationException;
19+
import com.yoti.api.client.KeyPairSource;
20+
import com.yoti.api.client.spi.remote.KeyStreamVisitor;
21+
import com.yoti.api.client.spi.remote.call.ResourceException;
22+
23+
import com.fasterxml.jackson.databind.DeserializationFeature;
24+
import com.fasterxml.jackson.databind.ObjectMapper;
25+
import io.jsonwebtoken.Jwts;
26+
27+
/**
28+
* The {@link AuthenticationTokenGenerator} is used for generation authorization tokens
29+
* that can be used for accessing Yoti services. An authorization token must have
30+
* a unique identifier, and an expiry timestamp. One or more scopes can be provided
31+
* to allow the authorization token access to different parts of Yoti systems.
32+
* <p>
33+
* The {@link AuthenticationTokenGenerator.Builder} can be accessed via {@code AuthorizationTokenGenerator.builder()}
34+
* method, and then configured via the fluent API.
35+
*/
36+
public class AuthenticationTokenGenerator {
37+
38+
private final String sdkId;
39+
private final KeyPair keyPair;
40+
private final Supplier<String> jwtIdSupplier;
41+
private final FormRequestClient formRequestClient;
42+
43+
private final URL authApiUrl;
44+
private final ObjectMapper objectMapper;
45+
46+
AuthenticationTokenGenerator(
47+
String sdkId,
48+
KeyPair keyPair,
49+
Supplier<String> jwtIdSupplier,
50+
FormRequestClient formRequestClient,
51+
ObjectMapper objectMapper) {
52+
this.sdkId = sdkId;
53+
this.keyPair = keyPair;
54+
this.jwtIdSupplier = jwtIdSupplier;
55+
this.formRequestClient = formRequestClient;
56+
this.objectMapper = objectMapper;
57+
58+
try {
59+
authApiUrl = new URL(System.getProperty(Properties.PROPERTY_YOTI_AUTH_URL, Properties.DEFAULT_YOTI_AUTH_URL));
60+
} catch (MalformedURLException e) {
61+
throw new IllegalStateException("Invalid Yoti auth url", e);
62+
}
63+
}
64+
65+
/**
66+
* Creates a new instance of {@link AuthenticationTokenGenerator.Builder}
67+
*
68+
* @return the builder
69+
*/
70+
public static AuthenticationTokenGenerator.Builder builder() {
71+
return new AuthenticationTokenGenerator.Builder();
72+
}
73+
74+
/**
75+
* Creates a new authentication token, using the supplied scopes and comment.
76+
*
77+
* @param scopes a list of scopes to be used by the authentication token
78+
* @return a {@link CreateAuthenticationTokenResponse} containing information about the created token.
79+
* @throws ResourceException if something was incorrect with the request to the Yoti authentication service
80+
* @throws IOException
81+
*/
82+
public CreateAuthenticationTokenResponse generate(List<String> scopes) throws ResourceException, IOException {
83+
notNullOrEmpty(scopes, "scopes");
84+
85+
String jwts = createSignedJwt(sdkId, keyPair, jwtIdSupplier, authApiUrl);
86+
87+
Map<String, String> formParams = new HashMap<>();
88+
formParams.put("grant_type", "client_credentials");
89+
formParams.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
90+
formParams.put("scope", String.join(" ", scopes));
91+
formParams.put("client_assertion", jwts);
92+
93+
byte[] responseBody = formRequestClient.performRequest(authApiUrl, "POST", formParams);
94+
95+
return objectMapper.readValue(responseBody, CreateAuthenticationTokenResponse.class);
96+
}
97+
98+
private String createSignedJwt(String sdkId, KeyPair keyPair, Supplier<String> jwtIdSupplier, URL authApiUrl) {
99+
String sdkIdProperty = String.format("sdk:%s", sdkId);
100+
OffsetDateTime now = OffsetDateTime.now();
101+
return Jwts.builder()
102+
.issuer(sdkIdProperty)
103+
.subject(sdkIdProperty)
104+
.id(jwtIdSupplier.get())
105+
.audience()
106+
.add(authApiUrl.toString())
107+
.and()
108+
.expiration(new Date(now.plusMinutes(5).toInstant().toEpochMilli()))
109+
.issuedAt(new Date(now.toInstant().toEpochMilli()))
110+
.header()
111+
.add("alg", "PS384")
112+
.add("typ", "JWT")
113+
.and()
114+
.signWith(keyPair.getPrivate(), Jwts.SIG.PS384)
115+
.compact();
116+
}
117+
118+
public static final class Builder {
119+
120+
private String sdkId;
121+
private KeyPairSource keyPairSource;
122+
private Supplier<String> jwtIdSupplier = () -> UUID.randomUUID().toString();
123+
124+
private Builder() {
125+
}
126+
127+
/**
128+
* Sets the SDK ID that the authorization token will be generated against.
129+
*
130+
* @param sdkId the SDK ID
131+
* @return the builder for method chaining.
132+
*/
133+
public Builder withSdkId(String sdkId) {
134+
this.sdkId = sdkId;
135+
return this;
136+
}
137+
138+
/**
139+
* Sets the {@link KeyPairSource} that will be used to load the {@link KeyPair}
140+
*
141+
* @param keyPairSource the key pair source that will be used to load the {@link KeyPair}
142+
* @return the builder for method chaining.
143+
*/
144+
public Builder withKeyPairSource(KeyPairSource keyPairSource) {
145+
this.keyPairSource = keyPairSource;
146+
return this;
147+
}
148+
149+
/**
150+
* Sets the supplier that will be used to generate a unique ID for the
151+
* authorization token. By default, this will be a UUID v4.
152+
*
153+
* @param jwtIdSupplier the supplier used for generating authorization token ID
154+
* @return the builder for method chaining.
155+
*/
156+
public Builder withJwtIdSupplier(Supplier<String> jwtIdSupplier) {
157+
this.jwtIdSupplier = jwtIdSupplier;
158+
return this;
159+
}
160+
161+
/**
162+
* Builds an {@link AuthenticationTokenGenerator} using the values supplied to the {@link Builder}.
163+
*
164+
* @return the configured {@link AuthenticationTokenGenerator}
165+
*/
166+
public AuthenticationTokenGenerator build() {
167+
notNullOrEmpty(sdkId, "sdkId");
168+
notNull(keyPairSource, "keyPairSource");
169+
notNull(jwtIdSupplier, "jwtIdSupplier");
170+
171+
ObjectMapper objectMapper = new ObjectMapper()
172+
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
173+
174+
return new AuthenticationTokenGenerator(
175+
sdkId,
176+
loadKeyPair(keyPairSource),
177+
jwtIdSupplier,
178+
new FormRequestClient(),
179+
objectMapper
180+
);
181+
}
182+
183+
private KeyPair loadKeyPair(KeyPairSource kpSource) throws InitialisationException {
184+
try {
185+
return kpSource.getFromStream(new KeyStreamVisitor());
186+
} catch (IOException e) {
187+
throw new InitialisationException("Cannot load key pair", e);
188+
}
189+
}
190+
191+
}
192+
193+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.yoti.auth;
2+
3+
public final class CreateAuthenticationTokenResponse {
4+
5+
private String accessToken;
6+
private String tokenType;
7+
private Integer expiresIn;
8+
private String scope;
9+
10+
/**
11+
* Returns the Yoti Authentication token used to perform requests to other Yoti services.
12+
*
13+
* @return the newly created access token
14+
*/
15+
public String getAccessToken() {
16+
return accessToken;
17+
}
18+
19+
/**
20+
* Returns the type of the newly generated authentication token.
21+
*
22+
* @return the token type
23+
*/
24+
public String getTokenType() {
25+
return tokenType;
26+
}
27+
28+
/**
29+
* Returns the amount of time (in seconds) in which the newly generated Authentication Token
30+
* will expire in.
31+
*
32+
* @return the time (in seconds) of when the token will expire
33+
*/
34+
public Integer getExpiresIn() {
35+
return expiresIn;
36+
}
37+
38+
/**
39+
* A whitespace delimited string of scopes that the Authentication token has.
40+
*
41+
* @return the scopes of the token as a whitespace delimited string
42+
*/
43+
public String getScope() {
44+
return scope;
45+
}
46+
47+
}

0 commit comments

Comments
 (0)