Skip to content

Commit 61a4e67

Browse files
authored
Merge pull request #78 from speakeasy-sdks/ms/machin-auth-support
machine auth support
2 parents 424198e + 6a93e14 commit 61a4e67

29 files changed

Lines changed: 2423 additions & 634 deletions

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,17 +170,17 @@ Use the [authenticateRequest](https://github.com/clerk/clerk-sdk-java/blob/main/
170170

171171
```java
172172
import java.net.http.HttpRequest;
173-
import com.clerk.backend_api.helpers.jwks.AuthenticateRequest;
174-
import com.clerk.backend_api.helpers.jwks.AuthenticateRequestOptions;
175-
import com.clerk.backend_api.helpers.jwks.RequestState;
173+
import com.clerk.backend_api.helpers.security.AuthenticateRequest;
174+
import com.clerk.backend_api.helpers.security.models.AuthenticateRequestOptions;
175+
import com.clerk.backend_api.helpers.security.models.RequestState;
176176

177177
public class UserAuthentication {
178178

179179
public static boolean isSignedIn(HttpRequest request) {
180180
RequestState requestState = AuthenticateRequest.authenticateRequest(request, AuthenticateRequestOptions
181-
.secretKey(System.getenv("CLERK_SECRET_KEY"))
182-
.authorizedParty("https://example.com")
183-
.build());
181+
.secretKey(System.getenv("CLERK_SECRET_KEY"))
182+
.authorizedParty("https://example.com")
183+
.build());
184184
return requestState.isSignedIn();
185185
}
186186
```

src/main/java/com/clerk/backend_api/helpers/jwks/AuthenticateRequest.java renamed to src/main/java/com/clerk/backend_api/helpers/security/AuthenticateRequest.java

Lines changed: 42 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1-
package com.clerk.backend_api.helpers.jwks;
2-
3-
import io.jsonwebtoken.Claims;
4-
import io.jsonwebtoken.impl.DefaultClaims;
1+
package com.clerk.backend_api.helpers.security;
2+
3+
import com.clerk.backend_api.helpers.security.models.AuthErrorReason;
4+
import com.clerk.backend_api.helpers.security.models.AuthenticateRequestOptions;
5+
import com.clerk.backend_api.helpers.security.models.RequestState;
6+
import com.clerk.backend_api.helpers.security.models.TokenType;
7+
import com.clerk.backend_api.helpers.security.models.TokenVerificationException;
8+
import com.clerk.backend_api.helpers.security.models.TokenVerificationResponse;
9+
import com.clerk.backend_api.helpers.security.models.VerifyTokenOptions;
10+
import java.io.IOException;
511
import java.net.HttpCookie;
6-
import java.util.ArrayList;
712
import java.util.HashMap;
813
import java.util.List;
914
import java.util.Map;
1015
import java.util.concurrent.TimeUnit;
1116

17+
import static com.clerk.backend_api.helpers.security.util.TokenTypeHelper.getTokenType;
18+
1219
/**
1320
* AuthenticateRequest - Helper methods to authenticate requests.
1421
*/
@@ -34,58 +41,29 @@ private AuthenticateRequest() {
3441
* WARNING: authenticateRequest is applicable in the context of Backend
3542
* APIs only.
3643
*/
37-
public static final RequestState authenticateRequest(Map<String, List<String>> requestHeaders, AuthenticateRequestOptions options) {
44+
public static RequestState authenticateRequest(Map<String, List<String>> requestHeaders, AuthenticateRequestOptions options) {
3845

3946
String sessionToken = getSessionToken(requestHeaders);
4047
if (sessionToken == null) {
41-
return RequestState.signedOut(AuthErrorReason.SESSION_TOKEN_MISSING);
42-
}
48+
return RequestState.signedOut(AuthErrorReason.SESSION_TOKEN_MISSING); }
4349

44-
VerifyTokenOptions verifyTokenOptions;
4550

46-
if (options.jwtKey().isPresent()) {
47-
verifyTokenOptions = VerifyTokenOptions //
48-
.jwtKey(options.jwtKey().get()) //
49-
.audience(options.audience()) //
50-
.authorizedParties(options.authorizedParties()) //
51-
.clockSkew(options.clockSkewInMs(), TimeUnit.MILLISECONDS) //
52-
.build();
53-
} else if (options.secretKey().isPresent()) {
54-
verifyTokenOptions = VerifyTokenOptions //
55-
.secretKey(options.secretKey().get()) //
56-
.audience(options.audience()) //
57-
.authorizedParties(options.authorizedParties()) //
58-
.clockSkew(options.clockSkewInMs(), TimeUnit.MILLISECONDS) //
59-
.build();
60-
} else {
61-
return RequestState.signedOut(AuthErrorReason.SECRET_KEY_MISSING);
51+
List<String> acceptsTokens = options.acceptsToken();
52+
TokenType tokenType = getTokenType(sessionToken);
53+
54+
if (!acceptsTokens.contains(tokenType.getType()) && !acceptsTokens.contains("any")) {
55+
return RequestState.signedOut(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED);
6256
}
6357

64-
try {
65-
Claims claims = VerifyToken.verifyToken(sessionToken, verifyTokenOptions);
66-
Map<String, Object> updatedClaimsMap = new HashMap<>(claims);
67-
68-
if ("2".equals(String.valueOf(claims.get("v")))) {
69-
Object orgObject = claims.get("o");
70-
if (orgObject instanceof Map) {
71-
@SuppressWarnings("unchecked")
72-
Map<String, Object> orgClaims = (Map<String, Object>) orgObject;
73-
74-
updatedClaimsMap.put("org_id", orgClaims.get("id"));
75-
updatedClaimsMap.put("org_slug", orgClaims.get("slg"));
76-
updatedClaimsMap.put("org_role", orgClaims.get("rol"));
77-
78-
List<String> orgPermissions = computeOrgPermissions(claims);
79-
if (!orgPermissions.isEmpty()) {
80-
updatedClaimsMap.put("org_permissions", orgPermissions);
81-
}
82-
}
83-
}
58+
VerifyTokenOptions verifyTokenOptions = buildVerifyTokenOptions(options);
8459

85-
Claims updatedClaims = new DefaultClaims(updatedClaimsMap);
86-
return RequestState.signedIn(sessionToken, updatedClaims);
60+
try {
61+
TokenVerificationResponse<?> tokenVerificationResponse = VerifyToken.verifyToken(sessionToken, verifyTokenOptions);
62+
return RequestState.signedIn(sessionToken, tokenVerificationResponse);
8763
} catch (TokenVerificationException e) {
8864
return RequestState.signedOut(e.reason());
65+
} catch (IOException | InterruptedException e) {
66+
return RequestState.signedOut(AuthErrorReason.INTERNAL_ERROR);
8967
}
9068

9169
}
@@ -120,42 +98,26 @@ private static String getSessionToken(Map<String, List<String>> requestHeaders)
12098
return null;
12199
}
122100

123-
@SuppressWarnings("unchecked")
124-
private static List<String> computeOrgPermissions(Claims claims) {
125-
String featuresStr = (String) claims.get("fea");
126-
if (featuresStr == null) {
127-
return new ArrayList<>();
128-
}
129-
String permissionsStr = (String) ((Map<String, Object>) claims.get("o")).get("per");
130-
String mappingsStr = (String) ((Map<String, Object>) claims.get("o")).get("fpm");
131101

132-
String[] features = featuresStr.split(",");
133-
String[] permissions = permissionsStr.split(",");
134-
String[] mappings = mappingsStr.split(",");
135102

136-
List<String> orgPermissions = new ArrayList<>();
137-
138-
for (int idx = 0; idx < mappings.length; idx++) {
139-
String mapping = mappings[idx];
140-
String[] featureParts = features[idx].split(":");
141-
if (featureParts.length != 2) continue;
142-
143-
String scope = featureParts[0];
144-
String feature = featureParts[1];
145-
146-
if (!scope.contains("o")) continue;
147-
148-
String binary = Integer.toBinaryString(Integer.parseInt(mapping)).replaceAll("^0+", "");
149-
String reversedBinary = new StringBuilder(binary).reverse().toString();
150-
151-
for (int i = 0; i < reversedBinary.length(); i++) {
152-
if (reversedBinary.charAt(i) == '1' && i < permissions.length) {
153-
orgPermissions.add("org:" + feature + ":" + permissions[i]);
154-
}
155-
}
103+
private static VerifyTokenOptions buildVerifyTokenOptions(AuthenticateRequestOptions options) {
104+
if (options.jwtKey().isPresent()) {
105+
return VerifyTokenOptions //
106+
.jwtKey(options.jwtKey().get()) //
107+
.audience(options.audience()) //
108+
.authorizedParties(options.authorizedParties()) //
109+
.clockSkew(options.clockSkewInMs(), TimeUnit.MILLISECONDS) //
110+
.build();
111+
} else if (options.secretKey().isPresent()) {
112+
return VerifyTokenOptions //
113+
.secretKey(options.secretKey().get()) //
114+
.audience(options.audience()) //
115+
.authorizedParties(options.authorizedParties()) //
116+
.clockSkew(options.clockSkewInMs(), TimeUnit.MILLISECONDS) //
117+
.build();
118+
} else {
119+
return new VerifyTokenOptions.Builder().build();
156120
}
157-
158-
return orgPermissions;
159121
}
160122

161123
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.clerk.backend_api.helpers.security;
2+
3+
import com.clerk.backend_api.helpers.security.models.TokenType;
4+
import com.clerk.backend_api.helpers.security.models.TokenVerificationException;
5+
import com.clerk.backend_api.helpers.security.models.TokenVerificationResponse;
6+
import com.clerk.backend_api.helpers.security.models.VerifyTokenOptions;
7+
import com.clerk.backend_api.helpers.security.token_verifiers.impl.APIKeyTokenVerifier;
8+
import com.clerk.backend_api.helpers.security.token_verifiers.impl.JwtSessionTokenVerifier;
9+
import com.clerk.backend_api.helpers.security.token_verifiers.impl.MachineTokenVerifier;
10+
import com.clerk.backend_api.helpers.security.token_verifiers.impl.OAuthTokenVerifier;
11+
import java.io.IOException;
12+
13+
import static com.clerk.backend_api.helpers.security.util.TokenTypeHelper.getTokenType;
14+
15+
/**
16+
* VerifyToken - Helper methods for verifying JSON Web Tokens (JWT).
17+
*/
18+
public final class VerifyToken {
19+
20+
private VerifyToken() {
21+
// prevent instantiation (this is a utility class)
22+
}
23+
24+
public static TokenVerificationResponse<?> verifyToken(String token, VerifyTokenOptions options)
25+
throws TokenVerificationException, IOException, InterruptedException {
26+
TokenType tokenType = getTokenType(token);
27+
if (tokenType == TokenType.API_KEY) {
28+
return new APIKeyTokenVerifier().verify(token, options);
29+
} else if (tokenType == TokenType.MACHINE_TOKEN) {
30+
return new MachineTokenVerifier().verify(token, options);
31+
} else if (tokenType == TokenType.OAUTH_TOKEN) {
32+
return new OAuthTokenVerifier().verify(token, options);
33+
} else
34+
return JwtSessionTokenVerifier.verify(token, options);
35+
}
36+
37+
}

src/main/java/com/clerk/backend_api/helpers/jwks/AuthErrorReason.java renamed to src/main/java/com/clerk/backend_api/helpers/security/models/AuthErrorReason.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.clerk.backend_api.helpers.jwks;
1+
package com.clerk.backend_api.helpers.security.models;
22

33
/**
44
* AuthErrorReason - The reason for request authentication failure.
@@ -10,12 +10,17 @@ public enum AuthErrorReason implements ErrorReason {
1010
"Could not retrieve session token. Please make sure that the __session cookie or the HTTP authorization header contain a Clerk-generated session JWT"),
1111
SECRET_KEY_MISSING(
1212
"secret-key-missing",
13-
"Missing Clerk Secret Key. Go to https://dashboard.clerk.com and get your key for your instance.");
14-
13+
"Missing Clerk Secret Key. Go to https://dashboard.clerk.com and get your key for your instance."),
14+
INTERNAL_ERROR(
15+
"internal-error",
16+
"An internal error occurred while processing the request. Please try again later."),
17+
TOKEN_TYPE_NOT_SUPPORTED(
18+
"token-type-not-supported",
19+
"The provided token type is not supported. Please use a valid session token, API key, machine token, or OAuth token.");
1520
private final String id;
1621
private final String message;
1722

18-
private AuthErrorReason(String id, String message) {
23+
AuthErrorReason(String id, String message) {
1924
this.id = id;
2025
this.message = message;
2126
}

src/main/java/com/clerk/backend_api/helpers/jwks/AuthStatus.java renamed to src/main/java/com/clerk/backend_api/helpers/security/models/AuthStatus.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.clerk.backend_api.helpers.jwks;
1+
package com.clerk.backend_api.helpers.security.models;
22

33
/**
44
* AuthStatus - The request authentication status.
@@ -9,7 +9,7 @@ public enum AuthStatus {
99

1010
private final String value;
1111

12-
private AuthStatus(String value) {
12+
AuthStatus(String value) {
1313
this.value = value;
1414
}
1515

src/main/java/com/clerk/backend_api/helpers/jwks/AuthenticateRequestOptions.java renamed to src/main/java/com/clerk/backend_api/helpers/security/models/AuthenticateRequestOptions.java

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
package com.clerk.backend_api.helpers.jwks;
1+
package com.clerk.backend_api.helpers.security.models;
22

33
import java.util.Collection;
44
import java.util.HashSet;
5+
import java.util.List;
56
import java.util.Optional;
67
import java.util.Set;
78
import java.util.concurrent.TimeUnit;
89

910
import com.clerk.backend_api.utils.Utils;
11+
import java.util.stream.Collectors;
1012

1113
/**
1214
* AuthenticateRequestOptions - Options to configure AuthenticateRequest.
@@ -20,6 +22,7 @@ public final class AuthenticateRequestOptions {
2022
private final Optional<String> audience;
2123
private final Set<String> authorizedParties;
2224
private final long clockSkewInMs;
25+
private final List<String> acceptsToken;
2326

2427
/**
2528
* Options to configure AuthenticateRequest.
@@ -41,7 +44,9 @@ public AuthenticateRequestOptions(
4144
Optional<String> jwtKey,
4245
Optional<String> audience,
4346
Set<String> authorizedParties,
44-
Optional<Long> clockSkewInMs) {
47+
Optional<Long> clockSkewInMs,
48+
Optional<List<String>> acceptsToken
49+
) {
4550

4651
Utils.checkNotNull(secretKey, "secretKey");
4752
Utils.checkNotNull(jwtKey, "jwtKey");
@@ -54,6 +59,9 @@ public AuthenticateRequestOptions(
5459
this.audience = audience;
5560
this.authorizedParties = authorizedParties;
5661
this.clockSkewInMs = clockSkewInMs.orElse(DEFAULT_CLOCK_SKEW_MS);
62+
this.acceptsToken = acceptsToken.orElse(TokenType.getAllTypes().stream().map(TokenType::getType).collect(
63+
Collectors.toList())
64+
);
5765
}
5866

5967
public Optional<String> secretKey() {
@@ -76,6 +84,10 @@ public long clockSkewInMs() {
7684
return clockSkewInMs;
7785
}
7886

87+
public List<String> acceptsToken() {
88+
return acceptsToken;
89+
}
90+
7991
public static Builder secretKey(String secretKey) {
8092
return Builder.withSecretKey(secretKey);
8193
}
@@ -84,12 +96,18 @@ public static Builder jwtKey(String jwtKey) {
8496
return Builder.withJwtKey(jwtKey);
8597
}
8698

99+
public static Builder acceptsToken(List<String> acceptsToken) {
100+
Utils.checkNotNull(acceptsToken, "acceptsToken");
101+
return new Builder().acceptsTokens(acceptsToken);
102+
}
103+
87104
public static final class Builder {
88105

89106
private Optional<String> secretKey = Optional.empty();
90107
private Optional<String> jwtKey = Optional.empty();
91108
private Optional<String> audience = Optional.empty();
92109
private Set<String> authorizedParties = new HashSet<>();
110+
private Optional<List<String>> acceptsToken = Optional.empty();
93111
private long clockSkewInMs = DEFAULT_CLOCK_SKEW_MS;
94112

95113
private Builder() {}
@@ -144,15 +162,18 @@ public Builder clockSkew(Optional<Long> duration, TimeUnit unit) {
144162
return clockSkew(DEFAULT_CLOCK_SKEW_MS, TimeUnit.MILLISECONDS);
145163
}
146164

147-
148-
165+
public Builder acceptsTokens(List<String> acceptsToken) {
166+
Utils.checkNotNull(acceptsToken, "acceptsToken");
167+
this.acceptsToken = Optional.of(acceptsToken);
168+
return this;
169+
}
149170

150171
public AuthenticateRequestOptions build() {
151172
return new AuthenticateRequestOptions(secretKey,
152173
jwtKey,
153174
audience,
154175
authorizedParties,
155-
Optional.of(clockSkewInMs));
176+
Optional.of(clockSkewInMs),acceptsToken);
156177
}
157178
}
158179
}

src/main/java/com/clerk/backend_api/helpers/jwks/ErrorReason.java renamed to src/main/java/com/clerk/backend_api/helpers/security/models/ErrorReason.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.clerk.backend_api.helpers.jwks;
1+
package com.clerk.backend_api.helpers.security.models;
22

33
/**
44
* ErrorReason - Interface implemented by AuthErrorReason and TokenVerificationErrorReason.

0 commit comments

Comments
 (0)