-
Notifications
You must be signed in to change notification settings - Fork 18
SDK-2771: Add new maven module for supporting the creation of Yoti au… #505
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||||||
|
|
||||||
| 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)); | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this list be checked for null or empty string?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed 👍 |
||||||
| formParams.put("client_assertion", jwts); | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why having a var if used once?
Suggested change
|
||||||
|
|
||||||
| 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; | ||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the
ObjectMapperbeing injected/exposed?The
CreateAuthenticationTokenResponseis defined by this module, shouldn't theObjectMapper(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 ofCreateAuthenticationTokenResponse?There was a problem hiding this comment.
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
ObjectMapperto the Relying Business, but it's done this way (and in other places in the SDK) to make testing with mocks easier in theAuthenticationTokenGenerator