Skip to content

Commit

Permalink
Stabilize authentication APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
blacelle committed Sep 21, 2024
1 parent 751a754 commit a4940b2
Show file tree
Hide file tree
Showing 58 changed files with 927 additions and 386 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/spotless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
Expand Down
59 changes: 59 additions & 0 deletions authorization/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>eu.solven.kumite</groupId>
<artifactId>aggregator</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>authorization</artifactId>
<description>Manage access_token and refresh_token</description>

<dependencies>
<dependency>
<groupId>eu.solven.kumite</groupId>
<artifactId>public</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<!-- Useful to parse the accessToken, to get the playerId-->
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.41.1</version>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

<!-- https://github.com/cowtowncoder/java-uuid-generator -->
<!-- Useful for deterministic UUID generation, e.g. in development environments-->
<dependency>
<groupId>com.fasterxml.uuid</groupId>
<artifactId>java-uuid-generator</artifactId>
<version>5.1.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import lombok.extern.slf4j.Slf4j;

/**
* User details, typically from an oauth2 provider
Expand All @@ -17,6 +18,7 @@
@Value
@Builder
@Jacksonized
@Slf4j
public class KumiteUser {
// Used to create contests
public static final UUID SERVER_ACCOUNTID = UUID.fromString("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF");
Expand All @@ -34,4 +36,5 @@ public class KumiteUser {
// Each account has a default playerId.
@NonNull
UUID playerId;

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package eu.solven.kumite.account.fake_player;

import java.util.Set;
import java.util.UUID;

import eu.solven.kumite.account.KumiteUser;
Expand Down Expand Up @@ -60,4 +61,8 @@ public static KumitePlayer fakePlayer(int i) {
return KumitePlayer.builder().playerId(fakePlayerId(i)).accountId(FAKE_ACCOUNT_ID).build();
}

public static Set<UUID> fakePlayers() {
return Set.of(FakePlayerTokens.FAKE_PLAYER_ID1, FakePlayerTokens.FAKE_PLAYER_ID2);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public ReactiveJwtDecoder jwtDecoder(Environment env, KumiteTokenService kumiteT
+ "` or spring.profiles.active="
+ IKumiteSpringProfiles.P_UNSAFE_SERVER);
} else if ("GENERATE".equals(secretKeySpec)) {
// if (env.acceptsProfiles(Profiles.of(IKumiteSpringProfiles.P_PRODMODE))) {
// throw new IllegalStateException("Can not GENERATE oauth2 signingKey in `prodmode`");
// }
log.warn("We generate a random signingKey");
secretKeySpec = kumiteTokenService.generateSignatureSecret().toJSONString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,27 @@
import com.nimbusds.jwt.SignedJWT;

import eu.solven.kumite.account.KumiteUser;
import eu.solven.kumite.login.AccessTokenHolder;
import eu.solven.kumite.login.AccessTokenWrapper;
import eu.solven.kumite.login.RefreshTokenWrapper;
import eu.solven.kumite.tools.IUuidGenerator;
import eu.solven.kumite.tools.JdkUuidGenerator;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class KumiteTokenService {
public static final String KEY_JWT_SIGNINGKEY = "kumite.login.signing-key";
// https://connect2id.com/products/server/docs/api/token#url
public static final String ENV_OAUTH2_ISSUER = "kumite.oauth2.issuer-base-url";

public static final String KEY_JWT_SIGNINGKEY = "kumite.oauth2.signing-key";
// Expect a value parsable by `Duration.parse`
// public static final String KEY_ACCESSTOKEN_EXP = "kumite.login.oauth2_exp";
// Expect a value parsable by `Duration.parse`
public static final String KEY_ACCESSTOKEN_EXP = "kumite.login.oauth2_exp";
// public static final String KEY_REFRESHTOKEN_EXP = "kumite.login.oauth2_exp";

final Environment env;
final IUuidGenerator uuidGenerator;
// BEWARE We would prefer a RSA KeyPair for PRD
final Supplier<OctetSequenceKey> supplierSymetricKey;

public KumiteTokenService(Environment env, IUuidGenerator uuidgenerator) {
Expand Down Expand Up @@ -74,7 +81,10 @@ JWK generateSignatureSecret() {
return jwk;
}

public String generateAccessToken(KumiteUser user, Set<UUID> playerIds, Duration accessTokenValidity) {
public String generateAccessToken(KumiteUser user,
Set<UUID> playerIds,
Duration accessTokenValidity,
boolean isRefreshToken) {
// Generating a Signed JWT
// https://auth0.com/blog/rs256-vs-hs256-whats-the-difference/
// https://security.stackexchange.com/questions/194830/recommended-asymmetric-algorithms-for-jwt
Expand All @@ -85,16 +95,19 @@ public String generateAccessToken(KumiteUser user, Set<UUID> playerIds, Duration

Instant now = Instant.now();

String issuer = getIssuer();
JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder().subject(user.getAccountId().toString())
.audience("Kumite-Server")
.issuer("https://kumite.com")
// https://connect2id.com/products/server/docs/api/token#url
.issuer(issuer)
// https://www.oauth.com/oauth2-servers/access-tokens/self-encoded-access-tokens/
// This JWTId is very important, as it could be used to ban some access_token used by some lost VM
// running some bot.
.jwtID(uuidGenerator.randomUUID().toString())
.issueTime(Date.from(now))
.notBeforeTime(Date.from(now))
.expirationTime(Date.from(now.plus(accessTokenValidity)))
.claim("refresh_token", isRefreshToken)
.claim("playerIds", playerIds);

SignedJWT signedJWT = new SignedJWT(headerBuilder.build(), claimsSetBuilder.build());
Expand All @@ -109,6 +122,15 @@ public String generateAccessToken(KumiteUser user, Set<UUID> playerIds, Duration
return signedJWT.serialize();
}

private String getIssuer() {
String issuerBaseUrl = env.getRequiredProperty(ENV_OAUTH2_ISSUER);
if ("NEEDS_TO_BE_DEFINED".equals(issuerBaseUrl)) {
throw new IllegalStateException("Need to setup %s".formatted(ENV_OAUTH2_ISSUER));
}
// This matches `/api/v1/oauth2/token` as route for token generation
return issuerBaseUrl + "/api/v1/oauth2";
}

/**
* Generates an access token corresponding to provided user entity based on configured settings. The generated
* access token can be used to perform tasks on behalf of the user on subsequent HTTP calls to the application until
Expand All @@ -122,18 +144,13 @@ public String generateAccessToken(KumiteUser user, Set<UUID> playerIds, Duration
* @return The generated JWT access token.
* @throws IllegalStateException
*/
public AccessTokenHolder wrapInJwtToken(KumiteUser user, UUID playerId) {
Duration accessTokenValidity = Duration.parse(env.getProperty(KEY_ACCESSTOKEN_EXP, "PT1H"));

if (accessTokenValidity.compareTo(Duration.parse("PT1H")) > 0) {
// This typically happens when generating a long-lives access_token for development properties
log.warn("Unusual expiry for accessToken: {}", accessTokenValidity);
}
public AccessTokenWrapper wrapInJwtAccessToken(KumiteUser user, UUID playerId) {
Duration accessTokenValidity = Duration.parse("PT1H");

String accessToken = generateAccessToken(user, Set.of(playerId), accessTokenValidity);
String accessToken = generateAccessToken(user, Set.of(playerId), accessTokenValidity, false);

// https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/
return AccessTokenHolder.builder()
return AccessTokenWrapper.builder()
.accessToken(accessToken)
.playerId(playerId)
.tokenType("Bearer")
Expand All @@ -142,4 +159,21 @@ public AccessTokenHolder wrapInJwtToken(KumiteUser user, UUID playerId) {

}

// https://stackoverflow.com/questions/38986005/what-is-the-purpose-of-a-refresh-token
// https://stackoverflow.com/questions/40555855/does-the-refresh-token-expire-and-if-so-when
public RefreshTokenWrapper wrapInJwtRefreshToken(KumiteUser user, Set<UUID> playerIds) {
Duration refreshTokenValidity = Duration.parse("P365D");

String accessToken = generateAccessToken(user, playerIds, refreshTokenValidity, true);

// https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/
return RefreshTokenWrapper.builder()
.refreshToken(accessToken)
.playerIds(playerIds)
.tokenType("Bearer")
.expiresIn(refreshTokenValidity.toSeconds())
.build();

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# unsafe_oauth2: this is used by both kumite-server and kumite-player

kumite.oauth2:
# This key is used to sign refresh_token and access_token
# We hardcode one in resource to make development easier
# Else, on each reboot, all access_token would be invalid
signing-key: '{"kty":"oct","kid":"d6ff447e-2a6e-4ef6-9e3d-792f2d23f11e","k":"bJpLdV8t1P_Pv9nay4zTVAU7C9VaBsR5pBtuAsAPkOU","alg":"HS256"}'
issuer-base-url: https://unsafe.oauth2.kumite
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# fake_server: we activate various unsafe mecanisms to make the server easier to run for development purposes

# The constant seed is useful to push contestsIds to be the same on each run
kumite.random.seed: 0

kumite.login:
signing-key: '{"kty":"oct","kid":"d6ff447e-2a6e-4ef6-9e3d-792f2d23f11e","k":"bJpLdV8t1P_Pv9nay4zTVAU7C9VaBsR5pBtuAsAPkOU","alg":"HS256"}'
kumite:
random.seed: 0
# This is the authorizationServer base url
oauth2.issuer-base-url: http://localhost:8080
47 changes: 39 additions & 8 deletions js/src/main/resources/static/ui/js/kumite-me.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// my-component.js
import {} from "vue";
import {ref} from "vue";

import { mapState } from "pinia";
import { useKumiteStore } from "./store.js";
Expand All @@ -14,18 +13,39 @@ components: {
},
computed: {
...mapState(useKumiteStore, ["nbAccountFetching", "account", "needsToLogin"]),
...mapState(useKumiteStore, {
players(store) {
return Object.values(store.players).filter((p) => p.accountId == this.account.accountId);
},
}),
},
setup(props) {
const store = useKumiteStore();

store.loadCurrentAccountPlayers();

const refreshToken = ref("");

return {};
const generateRefreshToken = function() {
console.debug("Generating a refresh_token");
async function fetchFromUrl(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("Rejected request for games url" + url);
}

const responseJson = await response.json();
const refreshTokenWrapper = responseJson;

console.info("refreshToken", refreshTokenWrapper);

refreshToken.value = refreshTokenWrapper;
} catch (e) {
console.error("Issue on Network: ", e);
exampleMovesMetadata.value.error = e;
}
}

fetchFromUrl(`/oauth2/token?refresh_token=true`);
};

return {generateRefreshToken, refreshToken};
},
template: /* HTML */ `
<div v-if="needsToLogin">You need to login</div>
Expand All @@ -40,6 +60,17 @@ components: {
<ul>
<li v-for="player in players"><KumitePlayerRef :playerId="player.playerId"/></li>
</ul>
<form>
You want to develop your own long-running robot?
<button type="button" @click="generateRefreshToken" class="btn btn-primary">Generate an refresh_token</button>
</form>
<div v-if="refreshToken">
refresh_token=`{{refreshToken.refresh_token}}`<br/>
(refreshToken)<br/>
(Save it now on your side, as it will not be saved in Kumite-Server)
</div>
</div>
`,
};
48 changes: 1 addition & 47 deletions js/src/main/resources/static/ui/js/kumite-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,49 +32,7 @@ export default {
store.loadPlayer(props.playerId);
});

const generateAccessToken = function() {
console.debug("Generating a long-lived access_token");
async function fetchFromUrl(url) {
try {
const response = await store.authenticatedFetch(url);
if (!response.ok) {
throw new Error("Rejected request for games url" + url);
}

const responseJson = await response.json();
const newExampleMoves = responseJson.moves;

console.info("Loaded example moves", responseJson);

// This convoluted `modify` is needed until we clarify how wo can edit the Ref from this method
// https://stackoverflow.com/questions/26957719/replace-object-value-without-replacing-reference
function modify(obj, newObj) {
Object.keys(obj).forEach(function (key) {
delete obj[key];
});

Object.keys(newObj).forEach(function (key) {
obj[key] = newObj[key];
});
}

exampleMovesMetadata.value.loaded = true;

// https://stackoverflow.com/questions/61452458/ref-vs-reactive-in-vue-3
modify(exampleMoves, newExampleMoves);
} catch (e) {
console.error("Issue on Network: ", e);
exampleMovesMetadata.value.error = e;
}
}

// const viewingPlayerId = "00000000-0000-0000-0000-000000000000";
// const playerId = viewingPlayerId;
const playerId = store.playingPlayerId;
fetchFromUrl(`/board/moves?contest_id=${props.contestId}&player_id=${playerId}`);
};

return {generateAccessToken};
return {};
},
template: /* HTML */ `
<div v-if="needsToLogin">You need to login</div>
Expand All @@ -89,10 +47,6 @@ This is a player managed by <KumiteAccountRef :accountId="player.accountId" />
<div v-else>
This is one of your players.
<form>
<button type="button" @click="generateAccessToken" class="btn btn-primary">Generate an access_token</button>
</form>
</div>
</div>
`,
Expand Down
Loading

0 comments on commit a4940b2

Please sign in to comment.