diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index eec52a17b..e77b02549 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -14,24 +14,6 @@
-
-
-
-
diff --git a/src/inttest/resources/config/application.yml b/src/inttest/resources/config/application.yml
index 03d7905e9..e30da0b47 100644
--- a/src/inttest/resources/config/application.yml
+++ b/src/inttest/resources/config/application.yml
@@ -29,6 +29,8 @@ faf-api:
jwt:
secret-key-path: test-pki-private.key
public-key-path: test-pki-public.key
+ faf-hydra-jwks-url: https://accounts.google.com/.well-known/openid-configuration
+ faf-hydra-issuer: https://hydra.test.faforever.com/
map:
target-directory: "build/cache/map/maps"
directory-preview-path-small: "build/cache/map_previews/small"
diff --git a/src/main/java/com/faforever/api/config/FafApiProperties.java b/src/main/java/com/faforever/api/config/FafApiProperties.java
index 74742f3bc..209a60857 100644
--- a/src/main/java/com/faforever/api/config/FafApiProperties.java
+++ b/src/main/java/com/faforever/api/config/FafApiProperties.java
@@ -53,6 +53,8 @@ public static class Jwt {
private Path publicKeyPath;
private int accessTokenValiditySeconds = 3600;
private int refreshTokenValiditySeconds = 3600;
+ private String fafHydraJwksUrl;
+ private String fafHydraIssuer;
}
@Data
diff --git a/src/main/java/com/faforever/api/config/security/oauth2/OAuthJwtConfig.java b/src/main/java/com/faforever/api/config/security/oauth2/OAuthJwtConfig.java
index c000b83f4..45cbaa28d 100644
--- a/src/main/java/com/faforever/api/config/security/oauth2/OAuthJwtConfig.java
+++ b/src/main/java/com/faforever/api/config/security/oauth2/OAuthJwtConfig.java
@@ -1,6 +1,7 @@
package com.faforever.api.config.security.oauth2;
import com.faforever.api.config.FafApiProperties;
+import com.faforever.api.security.FafMultiTokenStore;
import com.faforever.api.security.FafUserAuthenticationConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -9,7 +10,6 @@
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
-import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import java.io.IOException;
import java.nio.file.Files;
@@ -33,8 +33,8 @@ public DefaultTokenServices tokenServices(TokenStore tokenStore) {
}
@Bean
- public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
- return new JwtTokenStore(jwtAccessTokenConverter);
+ public TokenStore tokenStore(FafApiProperties properties, JwtAccessTokenConverter jwtAccessTokenConverter) {
+ return new FafMultiTokenStore(properties, jwtAccessTokenConverter);
}
@Bean
diff --git a/src/main/java/com/faforever/api/data/domain/Game.java b/src/main/java/com/faforever/api/data/domain/Game.java
index 2d486086b..25111b8e1 100644
--- a/src/main/java/com/faforever/api/data/domain/Game.java
+++ b/src/main/java/com/faforever/api/data/domain/Game.java
@@ -27,7 +27,7 @@
import javax.persistence.Table;
import javax.persistence.Transient;
import java.time.OffsetDateTime;
-import java.util.List;
+import java.util.Set;
@Entity
@Table(name = "game_stats")
@@ -47,9 +47,9 @@ public class Game {
private MapVersion mapVersion;
private String name;
private Validity validity;
- private List playerStats;
+ private Set playerStats;
private String replayUrl;
- private List reviews;
+ private Set reviews;
private GameReviewsSummary reviewsSummary;
@Id
@@ -105,7 +105,7 @@ public Validity getValidity() {
@OneToMany(mappedBy = "game")
@BatchSize(size = 1000)
- public List getPlayerStats() {
+ public Set getPlayerStats() {
return playerStats;
}
@@ -124,7 +124,7 @@ public String getReplayUrl() {
@OneToMany(mappedBy = "game")
@UpdatePermission(expression = Prefab.ALL)
@BatchSize(size = 1000)
- public List getReviews() {
+ public Set getReviews() {
return reviews;
}
@@ -135,4 +135,23 @@ public List getReviews() {
public GameReviewsSummary getReviewsSummary() {
return reviewsSummary;
}
+
+ /**
+ * This ManyToOne relationship leads to a double left outer join through Elide causing an additional full table
+ * scan on the matchmaker_queue table. Even though it has only 3 records, it causes MySql 5.7 and MySQL to run
+ * a list of all games > 1 min on prod where it was ~1 second before.
+ *
+ * This can be fixed by migrating to MariaDB.
+ */
+// private Integer matchmakerQueueId;
+//
+// @JoinTable(name = "matchmaker_queue_game",
+// joinColumns = @JoinColumn(name = "game_stats_id"),
+// inverseJoinColumns = @JoinColumn(name = "matchmaker_queue_id")
+// )
+// @ManyToOne(fetch = FetchType.LAZY)
+// @Nullable
+// public Integer getMatchmakerQueueId() {
+// return matchmakerQueueId;
+// }
}
diff --git a/src/main/java/com/faforever/api/data/domain/Leaderboard.java b/src/main/java/com/faforever/api/data/domain/Leaderboard.java
index 6f67ab227..72321b195 100644
--- a/src/main/java/com/faforever/api/data/domain/Leaderboard.java
+++ b/src/main/java/com/faforever/api/data/domain/Leaderboard.java
@@ -15,22 +15,22 @@ public class Leaderboard extends AbstractEntity {
public static final String TYPE_NAME = "leaderboard";
- private String technical_name;
- private String name_key;
- private String description_key;
+ private String technicalName;
+ private String nameKey;
+ private String descriptionKey;
@Column(name = "technical_name")
- public String getTechnical_name() {
- return technical_name;
+ public String getTechnicalName() {
+ return technicalName;
}
@Column(name = "name_key")
- public String getName_key() {
- return name_key;
+ public String getNameKey() {
+ return nameKey;
}
@Column(name = "description_key")
- public String getDescription_key() {
- return description_key;
+ public String getDescriptionKey() {
+ return descriptionKey;
}
}
diff --git a/src/main/java/com/faforever/api/security/FafMultiTokenStore.java b/src/main/java/com/faforever/api/security/FafMultiTokenStore.java
new file mode 100644
index 000000000..c75c77a24
--- /dev/null
+++ b/src/main/java/com/faforever/api/security/FafMultiTokenStore.java
@@ -0,0 +1,123 @@
+package com.faforever.api.security;
+
+import com.faforever.api.config.FafApiProperties;
+import org.springframework.security.jwt.Jwt;
+import org.springframework.security.jwt.JwtHelper;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+import org.springframework.security.oauth2.common.OAuth2RefreshToken;
+import org.springframework.security.oauth2.common.util.JsonParser;
+import org.springframework.security.oauth2.common.util.JsonParserFactory;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.security.oauth2.provider.token.TokenStore;
+import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
+import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
+import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStore;
+import org.springframework.stereotype.Component;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+@Component
+public class FafMultiTokenStore implements TokenStore {
+ private JsonParser objectMapper = JsonParserFactory.create();
+
+ /**
+ * The token store for token created by this API (using custom OAuth 2.0 without OpenID Connect)
+ */
+ private final JwtTokenStore classicTokenStore;
+
+ /**
+ * The token store for tokens created by Ory Hydra (OpenID Connect token customized for FAF)
+ */
+ private final JwkTokenStore hydraTokenStore;
+
+ private final FafApiProperties fafApiProperties;
+
+ public FafMultiTokenStore(FafApiProperties fafApiProperties, JwtAccessTokenConverter jwtAccessTokenConverter) {
+ this.fafApiProperties = fafApiProperties;
+
+ classicTokenStore = new JwtTokenStore(jwtAccessTokenConverter);
+ hydraTokenStore = new JwkTokenStore(fafApiProperties.getJwt().getFafHydraJwksUrl(), jwtAccessTokenConverter);
+ }
+
+ @Override
+ public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
+ return readAuthentication(token.getValue());
+ }
+
+ @Override
+ public OAuth2Authentication readAuthentication(String token) {
+ Jwt unverifiedJwt = JwtHelper.decode(token);
+ Map claims = objectMapper.parseMap(unverifiedJwt.getClaims());
+
+ if (Objects.equals(claims.get("iss"), fafApiProperties.getJwt().getFafHydraIssuer())) {
+ return hydraTokenStore.readAuthentication(token);
+ } else {
+ return classicTokenStore.readAuthentication(token);
+ }
+ }
+
+ @Override
+ public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
+ // no implementation, equal to JwtTokenStore
+ }
+
+ @Override
+ public OAuth2AccessToken readAccessToken(String tokenValue) {
+ try {
+ return classicTokenStore.readAccessToken(tokenValue);
+ } catch (Exception e) {
+ return hydraTokenStore.readAccessToken(tokenValue);
+ }
+ }
+
+ @Override
+ public void removeAccessToken(OAuth2AccessToken token) {
+ // no implementation, equal to JwtTokenStore
+ }
+
+ @Override
+ public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
+ // no implementation, equal to JwtTokenStore
+ }
+
+ @Override
+ public OAuth2RefreshToken readRefreshToken(String tokenValue) {
+ return null;
+ }
+
+ @Override
+ public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
+ return null;
+ }
+
+ @Override
+ public void removeRefreshToken(OAuth2RefreshToken token) {
+ // no implementation, equal to JwtTokenStore
+ }
+
+ @Override
+ public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
+ // no implementation, equal to JwtTokenStore
+ }
+
+ @Override
+ public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
+ // equal to JwtTokenStore
+ return null;
+ }
+
+ @Override
+ public Collection findTokensByClientIdAndUserName(String clientId, String userName) {
+ // equal to JwtTokenStore
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Collection findTokensByClientId(String clientId) {
+ // equal to JwtTokenStore
+ return Collections.emptySet();
+ }
+}
diff --git a/src/main/java/com/faforever/api/security/FafUserAuthenticationConverter.java b/src/main/java/com/faforever/api/security/FafUserAuthenticationConverter.java
index c0aea1f0c..0c8da4f0f 100644
--- a/src/main/java/com/faforever/api/security/FafUserAuthenticationConverter.java
+++ b/src/main/java/com/faforever/api/security/FafUserAuthenticationConverter.java
@@ -1,5 +1,6 @@
package com.faforever.api.security;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
@@ -7,12 +8,15 @@
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
import java.util.Collection;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.stream.Collectors;
/**
* Converts a {@link FafUserDetails} from and to an {@link Authentication} for use in a JWT token.
*/
+@Slf4j
public class FafUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
public static final String USER_ID_KEY = "user_id";
@@ -32,16 +36,36 @@ public class FafUserAuthenticationConverter extends DefaultUserAuthenticationCon
@Override
public Authentication extractAuthentication(Map map) {
- if (!map.containsKey(USER_ID_KEY)) {
- return null;
- }
+ if (map.containsKey(USER_ID_KEY)) {
+ log.debug("Access token is FAF legacy token");
+
+ int id = (Integer) map.get(USER_ID_KEY);
+ String username = (String) map.get(USERNAME);
+ boolean accountNonLocked = Optional.ofNullable((Boolean) map.get(NON_LOCKED)).orElse(true);
+ Collection extends GrantedAuthority> authorities = getAuthorities(map);
+ UserDetails user = new FafUserDetails(id, username, "N/A", accountNonLocked, authorities);
+
+ return new UsernamePasswordAuthenticationToken(user, "N/A", authorities);
+ } else {
+ Object sub = map.get("sub");
+
+ if (sub == null) {
+ log.debug("Access token has no user associated");
+ return null;
+ }
- int id = (Integer) map.get(USER_ID_KEY);
- String username = (String) map.get(USERNAME);
- boolean accountNonLocked = Optional.ofNullable((Boolean) map.get(NON_LOCKED)).orElse(true);
- Collection extends GrantedAuthority> authorities = getAuthorities(map);
- UserDetails user = new FafUserDetails(id, username, "N/A", accountNonLocked, authorities);
+ log.debug("Access token is FAF OpenID Connect token");
+ int id = Integer.parseInt((String) sub);
+ var ext = (Map) map.get("ext");
+ var roles = (List) ext.get("roles");
+
+ var authorities = roles.stream()
+ .map(role -> (GrantedAuthority) () -> "ROLE_" + role)
+ .collect(Collectors.toList());
+
+ UserDetails user = new FafUserDetails(id, "username", "N/A", false, authorities);
+ return new UsernamePasswordAuthenticationToken(user, "N/A", authorities);
+ }
- return new UsernamePasswordAuthenticationToken(user, "N/A", authorities);
}
}
diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml
index 830184697..fad2751c8 100644
--- a/src/main/resources/config/application-dev.yml
+++ b/src/main/resources/config/application-dev.yml
@@ -3,6 +3,8 @@ faf-api:
jwt:
secretKeyPath: ${JWT_PRIVATE_KEY_PATH:test-pki-private.key}
publicKeyPath: ${JWT_PUBLIC_KEY_PATH:test-pki-public.key}
+ fafHydraJwksUrl: ${JWT_FAF_HYDRA_JWKS_URL:https://hydra.test.faforever.com/.well-known/jwks.json}
+ fafHydraIssuer: ${JWT_FAF_HYDRA_ISSUER:https://hydra.test.faforever.com/}
map:
target-directory: ${MAP_UPLOAD_PATH:build/cache/map/maps}
directory-preview-path-small: ${MAP_PREVIEW_PATH_SMALL:build/cache/map_previews/small}
diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml
index 67be4f517..eeccd4dcb 100644
--- a/src/main/resources/config/application-prod.yml
+++ b/src/main/resources/config/application-prod.yml
@@ -2,6 +2,8 @@ faf-api:
jwt:
secretKeyPath: ${JWT_PRIVATE_KEY_PATH}
publicKeyPath: ${JWT_PUBLIC_KEY_PATH}
+ fafHydraJwksUrl: ${JWT_FAF_HYDRA_JWKS_URL}
+ fafHydraIssuer: ${JWT_FAF_HYDRA_ISSUER}
map:
target-directory: ${MAP_UPLOAD_PATH}
directory-preview-path-small: ${MAP_PREVIEW_PATH_SMALL}