Skip to content

Commit 27d9890

Browse files
Initial implementation
1 parent 695a068 commit 27d9890

File tree

10 files changed

+583
-0
lines changed

10 files changed

+583
-0
lines changed

pom.xml

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
<groupId>com.accedia</groupId>
8+
<artifactId>accedia-apple-auth</artifactId>
9+
<version>0.1.0</version>
10+
<packaging>jar</packaging>
11+
12+
<name>Apple Auth by Accedia</name>
13+
<description>
14+
A light weight library with the goal to provide simple, out of the box Apple Authentication.
15+
</description>
16+
17+
<url>https://github.com/Accedia/appleauth-java</url>
18+
19+
<developers>
20+
<developer>
21+
<name>Lazar Lazarov</name>
22+
<email>[email protected]</email>
23+
<organization>Accedia</organization>
24+
<organizationUrl>http://www.accedia.com</organizationUrl>
25+
</developer>
26+
</developers>
27+
28+
<build>
29+
<plugins>
30+
<plugin>
31+
<groupId>org.apache.maven.plugins</groupId>
32+
<artifactId>maven-compiler-plugin</artifactId>
33+
<configuration>
34+
<source>8</source>
35+
<target>8</target>
36+
</configuration>
37+
</plugin>
38+
39+
<plugin>
40+
<groupId>org.apache.maven.plugins</groupId>
41+
<artifactId>maven-source-plugin</artifactId>
42+
<executions>
43+
<execution>
44+
<id>attach-sources</id>
45+
<goals>
46+
<goal>jar-no-fork</goal>
47+
</goals>
48+
</execution>
49+
</executions>
50+
</plugin>
51+
52+
<plugin>
53+
<groupId>org.apache.maven.plugins</groupId>
54+
<artifactId>maven-javadoc-plugin</artifactId>
55+
<executions>
56+
<execution>
57+
<id>attach-javadocs</id>
58+
<goals>
59+
<goal>jar</goal>
60+
</goals>
61+
</execution>
62+
</executions>
63+
</plugin>
64+
65+
<!-- <plugin>-->
66+
<!-- <groupId>org.apache.maven.plugins</groupId>-->
67+
<!-- <artifactId>maven-gpg-plugin</artifactId>-->
68+
<!-- <version>1.6</version>-->
69+
<!-- <executions>-->
70+
<!-- <execution>-->
71+
<!-- <id>sign-artifacts</id>-->
72+
<!-- <phase>verify</phase>-->
73+
<!-- <goals>-->
74+
<!-- <goal>sign</goal>-->
75+
<!-- </goals>-->
76+
<!-- </execution>-->
77+
<!-- </executions>-->
78+
<!-- </plugin>-->
79+
80+
</plugins>
81+
</build>
82+
83+
<dependencies>
84+
<dependency>
85+
<groupId>com.auth0</groupId>
86+
<artifactId>java-jwt</artifactId>
87+
<version>3.10.3</version>
88+
</dependency>
89+
90+
<dependency>
91+
<groupId>com.auth0</groupId>
92+
<artifactId>jwks-rsa</artifactId>
93+
<version>0.11.0</version>
94+
</dependency>
95+
96+
<dependency>
97+
<groupId>com.google.api-client</groupId>
98+
<artifactId>google-api-client</artifactId>
99+
<version>1.30.9</version>
100+
</dependency>
101+
</dependencies>
102+
103+
104+
</project>
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package com.accedia.apple.auth;
2+
3+
import com.accedia.apple.auth.user.AppleAuthorizationToken;
4+
import com.accedia.apple.auth.user.UserData;
5+
import com.accedia.apple.auth.user.UserDataDeserializer;
6+
import com.auth0.jwt.JWT;
7+
import com.auth0.jwt.JWTVerifier;
8+
import com.auth0.jwt.algorithms.Algorithm;
9+
import com.auth0.jwt.interfaces.RSAKeyProvider;
10+
import com.google.api.client.auth.oauth2.*;
11+
import com.google.api.client.http.GenericUrl;
12+
import com.google.api.client.http.HttpTransport;
13+
import com.google.api.client.http.javanet.NetHttpTransport;
14+
import com.google.api.client.json.JsonFactory;
15+
import com.google.api.client.json.jackson2.JacksonFactory;
16+
import com.google.common.base.Supplier;
17+
import com.google.common.base.Suppliers;
18+
import com.google.common.collect.ImmutableList;
19+
20+
import javax.annotation.Nullable;
21+
import java.io.IOException;
22+
import java.security.interfaces.ECPrivateKey;
23+
import java.time.Instant;
24+
import java.util.Arrays;
25+
import java.util.Collection;
26+
import java.util.List;
27+
import java.util.Optional;
28+
import java.util.concurrent.TimeUnit;
29+
import java.util.stream.Collectors;
30+
31+
public class AppleAuthProvider {
32+
33+
private final static String DEFAULT_APPLE_AUTH_TOKEN_URL = "https://appleid.apple.com/auth/token";
34+
private final static String DEFAULT_APPLE_AUTH_AUTHORIZE_URL = "https://appleid.apple.com/auth/authorize";
35+
private final static long DEFAULT_SECRET_LIFE_IN_SEC = 20 * 60;
36+
private final static long DEFAULT_MAX_TIMEOUT_IN_SEC = 20;
37+
38+
private final String clientId;
39+
private final Supplier<ClientParametersAuthentication> appleClientParameters;
40+
private final SecretGenerator secretGenerator;
41+
private final ECPrivateKey ecPrivateKey;
42+
private final Supplier<Instant> nowSupplier;
43+
private final String keyId;
44+
private final String teamId;
45+
private final long secretLifeInSec;
46+
private final GenericUrl appleAuthTokenUrl;
47+
private final String appleAuthAuthorizationUrl;
48+
private UserDataDeserializer userDataDeserializer;
49+
private final List<String> appleUserScopes;
50+
private final String redirectUrl;
51+
private final HttpTransport httpTransport;
52+
private final JsonFactory jsonFactory;
53+
private final JWTVerifier jwtVerifier;
54+
55+
56+
/**
57+
* A constructor with minimum configuration. Uses default Apple URLs, http transport, json factory and timings.
58+
*
59+
* @param clientId A10-character key identifier obtained from your developer account.
60+
* (aka "Service ID" that is configured for “Sign In with Apple")
61+
* @param keyId A 10-character key identifier obtained from your developer account.
62+
* Configured for "Sign In with Apple".
63+
* @param teamId A 10-character key identifier obtained from your developer account.
64+
* @param secretGenerator A provider for the client secret as specified in apple auth id.
65+
* @param privateKey The private key as supplied by Apple.
66+
* @param redirectUrl URL to which the user will be redirected after successful verification.
67+
* You need to configure a verified domain and map the redirect URL to it.
68+
* Can’t be an IP address or localhost. Can be left null if url generation won't
69+
* be used.
70+
* @param scopes The apple required scopes. Values given here will determine the content of the
71+
* User Data returned in the tokens. Can be left null if url generation won't
72+
* be used.
73+
*/
74+
public AppleAuthProvider(String clientId, String keyId, String teamId, SecretGenerator secretGenerator,
75+
ECPrivateKey privateKey, @Nullable Collection<AppleUserScope> scopes,
76+
@Nullable String redirectUrl) {
77+
this(clientId,
78+
keyId,
79+
teamId,
80+
DEFAULT_APPLE_AUTH_TOKEN_URL,
81+
DEFAULT_APPLE_AUTH_AUTHORIZE_URL,
82+
new UserDataDeserializer(),
83+
new NetHttpTransport(),
84+
new JacksonFactory(),
85+
DEFAULT_MAX_TIMEOUT_IN_SEC,
86+
secretGenerator,
87+
privateKey,
88+
() -> Instant.now(),
89+
DEFAULT_SECRET_LIFE_IN_SEC,
90+
new AppleKeyProvider(),
91+
scopes,
92+
redirectUrl
93+
);
94+
}
95+
96+
/**
97+
* Constructor using no default values.
98+
*
99+
* @param clientId A10-character key identifier obtained from your developer account.
100+
* (aka "Service ID" that is configured for “Sign In with Apple")
101+
* @param keyId A 10-character key identifier obtained from your developer account.
102+
* Configured for "Sign In with Apple".
103+
* @param teamId A 10-character key identifier obtained from your developer account.
104+
* @param redirectUrl URL to which the user will be redirected after successful verification.
105+
* You need to configure a verified domain and map the redirect URL to it.
106+
* Can’t be an IP address or localhost.
107+
* @param scopes The apple required scopes. Values given here will determine the content of the
108+
* User Data returned
109+
* in the tokens.
110+
* @param ecPrivateKey The private key supplied by apple.
111+
* @param secretGenerator A provider for the client secret as specified in apple auth id.
112+
* @param secretLifeInSec The lifespan of the client secret in seconds.
113+
* @param appleAuthTokenUrl The apple oauth2 url for issuing Authentication and Verification tokens.
114+
* @param appleAuthAuthorizationUrl The apple url for beginning the Auth login.
115+
* @param userDataDeserializer A deserializer used to extract user data from the raw apple token response.
116+
* @param httpTransport The transport that will be used to make the token authenticate nad validate
117+
* requests.
118+
* @param jsonFactory The factory used to create the parser and generator for the tokens.
119+
* @param appleKeyProvider A provider that will be used to validate the token responses.
120+
* @param maxTimeoutInSec The Maximum allowed time for a http request before a timeout occurs.
121+
* @param nowSupplier Should return the current instance.
122+
*/
123+
public AppleAuthProvider(String clientId,
124+
String keyId,
125+
String teamId,
126+
String appleAuthTokenUrl,
127+
String appleAuthAuthorizationUrl,
128+
UserDataDeserializer userDataDeserializer,
129+
HttpTransport httpTransport,
130+
JsonFactory jsonFactory,
131+
long maxTimeoutInSec,
132+
SecretGenerator secretGenerator,
133+
ECPrivateKey ecPrivateKey,
134+
Supplier<Instant> nowSupplier,
135+
long secretLifeInSec,
136+
RSAKeyProvider appleKeyProvider,
137+
@Nullable Collection<AppleUserScope> scopes,
138+
@Nullable String redirectUrl
139+
) {
140+
this.clientId = clientId;
141+
this.secretGenerator = secretGenerator;
142+
this.ecPrivateKey = ecPrivateKey;
143+
this.nowSupplier = nowSupplier;
144+
this.keyId = keyId;
145+
this.teamId = teamId;
146+
this.secretLifeInSec = secretLifeInSec;
147+
this.userDataDeserializer = userDataDeserializer;
148+
this.appleUserScopes = scopes != null ? ImmutableList.copyOf(
149+
scopes.stream().map(AppleUserScope::getLiteral).collect(Collectors.toList())
150+
) : null;
151+
this.appleAuthTokenUrl = new GenericUrl(appleAuthTokenUrl);
152+
this.jsonFactory = jsonFactory;
153+
this.appleAuthAuthorizationUrl = appleAuthAuthorizationUrl;
154+
appleClientParameters = Suppliers.memoizeWithExpiration(this::generateClientAuthParameterSet,
155+
this.secretLifeInSec - maxTimeoutInSec,
156+
TimeUnit.SECONDS
157+
);
158+
this.redirectUrl = redirectUrl;
159+
this.httpTransport = httpTransport;
160+
161+
Algorithm validationAlg = Algorithm.RSA256(appleKeyProvider);
162+
this.jwtVerifier = JWT.require(validationAlg)
163+
.build();
164+
165+
}
166+
167+
private ClientParametersAuthentication generateClientAuthParameterSet() {
168+
String newSecret = secretGenerator.generateSecret(ecPrivateKey, keyId, teamId, clientId,
169+
nowSupplier.get(), secretLifeInSec);
170+
return new ClientParametersAuthentication(clientId, newSecret);
171+
}
172+
173+
/**
174+
* Generates a login link that will take the user Apple's authentication portal.
175+
* @param state A string that will be passed back when the user eventually makes their way to the redirect url.
176+
* This will be automatically escaped.
177+
* @return A URL ready for embedding.
178+
*/
179+
public String getLoginLink(String state) {
180+
return new AuthorizationCodeRequestUrl(appleAuthAuthorizationUrl, clientId)
181+
.setRedirectUri(this.redirectUrl)
182+
.setResponseTypes(Arrays.asList("code", "id_token"))
183+
.setScopes(appleUserScopes)
184+
.setState(state)
185+
.set("response_mode", "form_post")//Could be parameterized based on scope.
186+
.build();
187+
}
188+
189+
/**
190+
* Makes an authorisation request. Retrieves an User's date from Apple. Use this object to create users or sessions.
191+
* @param authCode Received from Apple after successfully redirecting the use.
192+
* @throws IOException
193+
*/
194+
public AppleAuthorizationToken makeNewAuthorisationTokenRequest(String authCode) throws IOException {
195+
AuthorizationCodeTokenRequest authoriseTokenRequest
196+
= new AuthorizationCodeTokenRequest(httpTransport, jsonFactory, appleAuthTokenUrl, authCode);
197+
198+
return executeTokenRequest(authoriseTokenRequest);
199+
}
200+
201+
/**
202+
* Verifies if a token is valid.
203+
* Use this method to check daily if the user is still signed in on your app using Apple ID.
204+
* @param refreshToken
205+
* @return
206+
* @throws IOException
207+
*/
208+
public AppleAuthorizationToken makeNewRefreshTokenRequest(String refreshToken) throws IOException {
209+
RefreshTokenRequest refreshTokenRequest
210+
= new RefreshTokenRequest(httpTransport, jsonFactory, appleAuthTokenUrl, refreshToken);
211+
212+
return executeTokenRequest(refreshTokenRequest);
213+
}
214+
215+
private AppleAuthorizationToken executeTokenRequest(TokenRequest tokenRequest) throws IOException {
216+
217+
tokenRequest.setClientAuthentication(appleClientParameters.get());
218+
TokenResponse tokenResponse = tokenRequest.execute();
219+
Optional<String> idToken = Optional.ofNullable(tokenResponse.get("id_token")).map(Object::toString);
220+
idToken.ifPresent(this::validateToken);
221+
Optional<UserData> userData = idToken.map(userDataDeserializer::getUserDataFromIdToken);
222+
return new AppleAuthorizationToken(
223+
tokenResponse.getAccessToken(),
224+
tokenResponse.getExpiresInSeconds(),
225+
idToken.orElse(null),
226+
tokenResponse.getRefreshToken(),
227+
userData.orElse(null));
228+
}
229+
230+
private void validateToken(String token) {
231+
jwtVerifier.verify(token);
232+
}
233+
234+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.accedia.apple.auth;
2+
3+
import com.auth0.jwk.JwkException;
4+
import com.auth0.jwk.JwkProvider;
5+
import com.auth0.jwk.JwkProviderBuilder;
6+
import com.auth0.jwt.interfaces.RSAKeyProvider;
7+
import java.security.interfaces.RSAPrivateKey;
8+
import java.security.interfaces.RSAPublicKey;
9+
import java.util.concurrent.TimeUnit;
10+
11+
public class AppleKeyProvider implements RSAKeyProvider {
12+
13+
private static final String DEFAULT_APPLE_KEY_URL = "https://appleid.apple.com/auth/keys";
14+
private static final long DEFAULT_KEY_VALIDITY_IN_SEC = 60 * 60;
15+
private final JwkProvider jwkProvider;
16+
17+
public AppleKeyProvider() {
18+
JwkProviderBuilder providerBuilder = new JwkProviderBuilder(DEFAULT_APPLE_KEY_URL);
19+
jwkProvider = providerBuilder.cached(10, DEFAULT_KEY_VALIDITY_IN_SEC, TimeUnit.SECONDS).build();
20+
}
21+
22+
@Override
23+
public RSAPublicKey getPublicKeyById(String s) {
24+
try {
25+
return (RSAPublicKey) jwkProvider.get(s).getPublicKey();
26+
} catch (JwkException e) {
27+
throw new RuntimeException("Error occurred while retrieving Apple Public Keys.", e);
28+
}
29+
}
30+
31+
@Override
32+
public RSAPrivateKey getPrivateKey() {
33+
throw new UnsupportedOperationException("Can't get apple private key.");
34+
}
35+
36+
@Override
37+
public String getPrivateKeyId() {
38+
throw new UnsupportedOperationException("Can't get apple private key.");
39+
}
40+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.accedia.apple.auth;
2+
3+
public enum AppleUserScope {
4+
EMAIL("email"),
5+
NAME("name");
6+
7+
private final String literal;
8+
9+
AppleUserScope(String literal)
10+
{
11+
this.literal = literal;
12+
}
13+
14+
String getLiteral() {
15+
return literal;
16+
}
17+
}

0 commit comments

Comments
 (0)