Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<modules>
<module>yoti-sdk-parent</module>
<module>yoti-sdk-api</module>
<module>yoti-sdk-auth</module>
<module>yoti-sdk-sandbox</module>
<module>yoti-sdk-spring-boot-auto-config</module>
<module>yoti-sdk-spring-security</module>
Expand Down
113 changes: 113 additions & 0 deletions yoti-sdk-auth/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.yoti</groupId>
<artifactId>yoti-sdk-parent</artifactId>
<version>4.0.0-SNAPSHOT</version>
<relativePath>../yoti-sdk-parent</relativePath>
</parent>

<artifactId>yoti-sdk-auth</artifactId>

<properties>
<jjwt.version>0.13.0</jjwt.version>
</properties>

<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>com.yoti</groupId>
<artifactId>yoti-sdk-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>

<!-- Testing dependencies -->
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-enforcer-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
</plugin>
<plugin>
<artifactId>maven-source-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

<reporting>
<plugins>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
</plugin>
</plugins>
</reporting>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package com.yoti.auth;

import static com.yoti.validation.Validation.notNull;
import static com.yoti.validation.Validation.notNullOrEmpty;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyPair;
import java.time.OffsetDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Supplier;

import com.yoti.api.client.InitialisationException;
import com.yoti.api.client.KeyPairSource;
import com.yoti.api.client.spi.remote.KeyStreamVisitor;
import com.yoti.api.client.spi.remote.call.ResourceException;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;

/**
* The {@link AuthenticationTokenGenerator} is used for generation authorization tokens
* that can be used for accessing Yoti services. An authorization token must have
* a unique identifier, and an expiry timestamp. One or more scopes can be provided
* to allow the authorization token access to different parts of Yoti systems.
* <p>
* The {@link AuthenticationTokenGenerator.Builder} can be accessed via {@code AuthorizationTokenGenerator.builder()}
* method, and then configured via the fluent API.
*/
public class AuthenticationTokenGenerator {

private final String sdkId;
private final KeyPair keyPair;
private final Supplier<String> jwtIdSupplier;
private final FormRequestClient formRequestClient;

private final URL authApiUrl;
private final ObjectMapper objectMapper;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the ObjectMapper being injected/exposed?

The CreateAuthenticationTokenResponse is defined by this module, shouldn't the ObjectMapper (and it's config) also be owned/defined by this module? What would be the point of the client providing their own instance config if they do not control the annotations/layout of CreateAuthenticationTokenResponse?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't expose the ObjectMapper to the Relying Business, but it's done this way (and in other places in the SDK) to make testing with mocks easier in the AuthenticationTokenGenerator


AuthenticationTokenGenerator(
String sdkId,
KeyPair keyPair,
Supplier<String> jwtIdSupplier,
FormRequestClient formRequestClient,
ObjectMapper objectMapper) {
this.sdkId = sdkId;
this.keyPair = keyPair;
this.jwtIdSupplier = jwtIdSupplier;
this.formRequestClient = formRequestClient;
this.objectMapper = objectMapper;

try {
authApiUrl = new URL(System.getProperty(Properties.PROPERTY_YOTI_AUTH_URL, Properties.DEFAULT_YOTI_AUTH_URL));
} catch (MalformedURLException e) {
throw new IllegalStateException("Invalid Yoti auth url", e);
}
}

/**
* Creates a new instance of {@link AuthenticationTokenGenerator.Builder}
*
* @return the builder
*/
public static AuthenticationTokenGenerator.Builder builder() {
return new AuthenticationTokenGenerator.Builder();
}

/**
* Creates a new authentication token, using the supplied scopes and comment.
*
* @param scopes a list of scopes to be used by the authentication token
* @return a {@link CreateAuthenticationTokenResponse} containing information about the created token.
* @throws ResourceException if something was incorrect with the request to the Yoti authentication service
* @throws IOException
*/
public CreateAuthenticationTokenResponse generate(List<String> scopes) throws ResourceException, IOException {
notNullOrEmpty(scopes, "scopes");

String jwts = createSignedJwt(sdkId, keyPair, jwtIdSupplier, authApiUrl);

Map<String, String> formParams = new HashMap<>();
formParams.put("grant_type", "client_credentials");
formParams.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
formParams.put("scope", String.join(" ", scopes));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this list be checked for null or empty string?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed 👍

formParams.put("client_assertion", jwts);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why having a var if used once?

Suggested change
formParams.put("client_assertion", jwts);
formParams.put("client_assertion", createSignedJwt(sdkId, keyPair, jwtIdSupplier, authApiUrl));


byte[] responseBody = formRequestClient.performRequest(authApiUrl, "POST", formParams);

return objectMapper.readValue(responseBody, CreateAuthenticationTokenResponse.class);
}

private String createSignedJwt(String sdkId, KeyPair keyPair, Supplier<String> jwtIdSupplier, URL authApiUrl) {
String sdkIdProperty = String.format("sdk:%s", sdkId);
OffsetDateTime now = OffsetDateTime.now();
return Jwts.builder()
.issuer(sdkIdProperty)
.subject(sdkIdProperty)
.id(jwtIdSupplier.get())
.audience()
.add(authApiUrl.toString())
.and()
.expiration(new Date(now.plusMinutes(5).toInstant().toEpochMilli()))
.issuedAt(new Date(now.toInstant().toEpochMilli()))
.header()
.add("alg", "PS384")
.add("typ", "JWT")
.and()
.signWith(keyPair.getPrivate(), Jwts.SIG.PS384)
.compact();
}

public static final class Builder {

private String sdkId;
private KeyPairSource keyPairSource;
private Supplier<String> jwtIdSupplier = () -> UUID.randomUUID().toString();

private Builder() {
}

/**
* Sets the SDK ID that the authorization token will be generated against.
*
* @param sdkId the SDK ID
* @return the builder for method chaining.
*/
public Builder withSdkId(String sdkId) {
this.sdkId = sdkId;
return this;
}

/**
* Sets the {@link KeyPairSource} that will be used to load the {@link KeyPair}
*
* @param keyPairSource the key pair source that will be used to load the {@link KeyPair}
* @return the builder for method chaining.
*/
public Builder withKeyPairSource(KeyPairSource keyPairSource) {
this.keyPairSource = keyPairSource;
return this;
}

/**
* Sets the supplier that will be used to generate a unique ID for the
* authorization token. By default, this will be a UUID v4.
*
* @param jwtIdSupplier the supplier used for generating authorization token ID
* @return the builder for method chaining.
*/
public Builder withJwtIdSupplier(Supplier<String> jwtIdSupplier) {
this.jwtIdSupplier = jwtIdSupplier;
return this;
}

/**
* Builds an {@link AuthenticationTokenGenerator} using the values supplied to the {@link Builder}.
*
* @return the configured {@link AuthenticationTokenGenerator}
*/
public AuthenticationTokenGenerator build() {
notNullOrEmpty(sdkId, "sdkId");
notNull(keyPairSource, "keyPairSource");
notNull(jwtIdSupplier, "jwtIdSupplier");

ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

return new AuthenticationTokenGenerator(
sdkId,
loadKeyPair(keyPairSource),
jwtIdSupplier,
new FormRequestClient(),
objectMapper
);
}

private KeyPair loadKeyPair(KeyPairSource kpSource) throws InitialisationException {
try {
return kpSource.getFromStream(new KeyStreamVisitor());
} catch (IOException e) {
throw new InitialisationException("Cannot load key pair", e);
}
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.yoti.auth;

public final class CreateAuthenticationTokenResponse {

private String accessToken;
private String tokenType;
private Integer expiresIn;
private String scope;

/**
* Returns the Yoti Authentication token used to perform requests to other Yoti services.
*
* @return the newly created access token
*/
public String getAccessToken() {
return accessToken;
}

/**
* Returns the type of the newly generated authentication token.
*
* @return the token type
*/
public String getTokenType() {
return tokenType;
}

/**
* Returns the amount of time (in seconds) in which the newly generated Authentication Token
* will expire in.
*
* @return the time (in seconds) of when the token will expire
*/
public Integer getExpiresIn() {
return expiresIn;
}

/**
* A whitespace delimited string of scopes that the Authentication token has.
*
* @return the scopes of the token as a whitespace delimited string
*/
public String getScope() {
return scope;
}

}
Loading