Skip to content

Commit a49b9f4

Browse files
committed
Add Google DEV OAuth2 app
1 parent 2ab3c00 commit a49b9f4

File tree

8 files changed

+131
-64
lines changed

8 files changed

+131
-64
lines changed

contest-core/src/main/java/eu/solven/kumite/player/ContestPlayersRegistry.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private void registerContender(Contest contest, UUID playerId) {
7373
if (game.canAcceptPlayer(contest, player)) {
7474
boolean registeredInBoard = contestPlayersRepository.registerContender(contestId, playerId);
7575
if (registeredInBoard) {
76-
log.info(
76+
log.debug(
7777
"Skip `board.registerContender` as already managed by `contestPlayersRepository.registerContender`");
7878
} else {
7979
IKumiteBoard board = contest.getBoard().get();

server/src/main/java/eu/solven/kumite/app/webflux/api/KumiteLoginController.java

+7-37
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@
3535
import eu.solven.kumite.account.KumiteUser;
3636
import eu.solven.kumite.account.KumiteUserRawRaw;
3737
import eu.solven.kumite.account.KumiteUsersRegistry;
38-
import eu.solven.kumite.account.login.IKumiteTestConstants;
3938
import eu.solven.kumite.app.IKumiteSpringProfiles;
4039
import eu.solven.kumite.oauth2.authorizationserver.KumiteTokenService;
4140
import eu.solven.kumite.player.IAccountPlayersRegistry;
4241
import eu.solven.kumite.player.KumitePlayer;
4342
import eu.solven.kumite.security.LoginRouteButNotAuthenticatedException;
43+
import eu.solven.kumite.security.oauth2.KumiteOAuth2UserService;
4444
import lombok.AllArgsConstructor;
4545
import lombok.extern.slf4j.Slf4j;
4646
import reactor.core.publisher.Mono;
@@ -56,9 +56,6 @@
5656
@AllArgsConstructor
5757
@Slf4j
5858
public class KumiteLoginController {
59-
public static final String PROVIDERID_GITHUB = "github";
60-
@Deprecated
61-
public static final String PROVIDERID_TEST = IKumiteTestConstants.PROVIDERID_TEST;
6259

6360
final InMemoryReactiveClientRegistrationRepository clientRegistrationRepository;
6461

@@ -127,6 +124,12 @@ public Mono<Map> basic() {
127124
"/html/login?success"));
128125
}
129126

127+
private KumiteUser userFromOAuth2(OAuth2User o) {
128+
KumiteUserRawRaw rawRaw = KumiteOAuth2UserService.oauth2ToRawRaw(o);
129+
KumiteUser user = usersRegistry.getUser(rawRaw);
130+
return user;
131+
}
132+
130133
@GetMapping("/user")
131134
public Mono<KumiteUser> user() {
132135
return ReactiveSecurityContextHolder.getContext().map(sc -> {
@@ -150,39 +153,6 @@ private KumiteUser userFromUsername(UsernamePasswordAuthenticationToken username
150153
return user;
151154
}
152155

153-
private KumiteUser userFromOAuth2(OAuth2User o) {
154-
String providerId = guessProviderId(o);
155-
String sub = getSub(providerId, o);
156-
KumiteUserRawRaw rawRaw = KumiteUserRawRaw.builder().providerId(providerId).sub(sub).build();
157-
KumiteUser user = usersRegistry.getUser(rawRaw);
158-
return user;
159-
}
160-
161-
private String getSub(String providerId, OAuth2User o) {
162-
if (PROVIDERID_GITHUB.equals(providerId)) {
163-
Object sub = o.getAttribute("id");
164-
if (sub == null) {
165-
throw new IllegalStateException("Invalid sub: " + sub);
166-
}
167-
return sub.toString();
168-
} else if (PROVIDERID_TEST.equals(providerId)) {
169-
Object sub = o.getAttribute("id");
170-
if (sub == null) {
171-
throw new IllegalStateException("Invalid sub: " + sub);
172-
}
173-
return sub.toString();
174-
} else {
175-
throw new IllegalStateException("Not managed providerId: " + providerId);
176-
}
177-
}
178-
179-
private String guessProviderId(OAuth2User o) {
180-
if (PROVIDERID_TEST.equals(o.getAttribute("providerId"))) {
181-
return PROVIDERID_TEST;
182-
}
183-
return PROVIDERID_GITHUB;
184-
}
185-
186156
@GetMapping("/oauth2/token")
187157
public Mono<?> token(@RequestParam(name = "player_id", required = false) String rawPlayerId,
188158
@RequestParam(name = "refresh_token", defaultValue = "false") boolean requestRefreshToken) {

server/src/main/java/eu/solven/kumite/security/SocialWebFluxSecurity.java

+24-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import java.util.Map;
55
import java.util.concurrent.ConcurrentHashMap;
66

7-
import org.springframework.boot.autoconfigure.web.ServerProperties;
87
import org.springframework.context.annotation.Bean;
98
import org.springframework.context.annotation.Import;
109
import org.springframework.core.Ordered;
@@ -19,10 +18,15 @@
1918
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
2019
import org.springframework.security.core.userdetails.User;
2120
import org.springframework.security.core.userdetails.UserDetails;
21+
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
22+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
23+
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
24+
import org.springframework.security.oauth2.core.user.OAuth2User;
2225
import org.springframework.security.oauth2.jwt.Jwt;
2326
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
2427
import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint;
2528
import org.springframework.security.web.server.SecurityWebFilterChain;
29+
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
2630
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
2731
import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler;
2832
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
@@ -48,14 +52,23 @@
4852
@Slf4j
4953
public class SocialWebFluxSecurity {
5054

55+
// https://github.com/spring-projects/spring-security/issues/15846
56+
@Bean
57+
public OidcReactiveOAuth2UserService oidcReactiveOAuth2UserService(
58+
ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService) {
59+
OidcReactiveOAuth2UserService oidcReactiveOAuth2UserService = new OidcReactiveOAuth2UserService();
60+
61+
oidcReactiveOAuth2UserService.setOauth2UserService(oauth2UserService);
62+
63+
return oidcReactiveOAuth2UserService;
64+
}
65+
5166
// https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials/resource-server_with_ui
5267
// https://stackoverflow.com/questions/74744901/default-401-instead-of-redirecting-for-oauth2-login-spring-security
5368
// `-1` as this has to be used in priority aver the API securityFilterChain
5469
@Order(Ordered.LOWEST_PRECEDENCE - 1)
5570
@Bean
56-
public SecurityWebFilterChain configureUi(ServerProperties serverProperties,
57-
ServerHttpSecurity http,
58-
Environment env) {
71+
public SecurityWebFilterChain configureUi(ServerHttpSecurity http, Environment env) {
5972

6073
boolean isFakeUser = env.acceptsProfiles(Profiles.of(IKumiteSpringProfiles.P_FAKEUSER));
6174
if (isFakeUser) {
@@ -152,7 +165,11 @@ public SecurityWebFilterChain configureUi(ServerProperties serverProperties,
152165
.oauth2Login(oauth2 -> {
153166
String loginSuccess = "/html/login?success";
154167
oauth2.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler(loginSuccess));
168+
169+
String loginError = "/html/login?error";
170+
oauth2.authenticationFailureHandler(new RedirectServerAuthenticationFailureHandler(loginError));
155171
})
172+
// .oauth2Client(oauth2 -> oauth2.)
156173

157174
.httpBasic(basic -> {
158175
if (isFakeUser) {
@@ -200,9 +217,9 @@ private void configureBasicForFakeUser(HttpBasicSpec basic) {
200217
*/
201218
@Order(Ordered.LOWEST_PRECEDENCE)
202219
@Bean
203-
public SecurityWebFilterChain configureApi(ServerHttpSecurity http,
204-
Environment env,
205-
ReactiveJwtDecoder jwtDecoder) {
220+
public SecurityWebFilterChain configureApi(Environment env,
221+
ReactiveJwtDecoder jwtDecoder,
222+
ServerHttpSecurity http) {
206223

207224
boolean isFakeUser = env.acceptsProfiles(Profiles.of(IKumiteSpringProfiles.P_FAKEUSER));
208225
if (isFakeUser) {

server/src/main/java/eu/solven/kumite/security/oauth2/KumiteOAuth2UserService.java

+78-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package eu.solven.kumite.security.oauth2;
22

33
import java.net.URI;
4+
import java.net.URISyntaxException;
5+
import java.net.URL;
46

7+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
58
import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService;
69
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
710
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@@ -13,6 +16,7 @@
1316
import eu.solven.kumite.account.KumiteUserRaw.KumiteUserRawBuilder;
1417
import eu.solven.kumite.account.KumiteUserRawRaw;
1518
import eu.solven.kumite.account.KumiteUsersRegistry;
19+
import eu.solven.kumite.account.login.IKumiteTestConstants;
1620
import graphql.VisibleForTesting;
1721
import lombok.RequiredArgsConstructor;
1822
import lombok.SneakyThrows;
@@ -33,6 +37,11 @@
3337
@Service
3438
@RequiredArgsConstructor
3539
public class KumiteOAuth2UserService extends DefaultReactiveOAuth2UserService {
40+
public static final String PROVIDERID_GITHUB = "github";
41+
public static final String PROVIDERID_GOOGLE = "google";
42+
43+
@Deprecated
44+
public static final String PROVIDERID_TEST = IKumiteTestConstants.PROVIDERID_TEST;
3645

3746
// private final AccountsStore accountsStore;
3847
final KumiteUsersRegistry usersRegistry;
@@ -57,23 +66,18 @@ private Mono<OAuth2User> processOAuth2User(OAuth2UserRequest oAuth2UserRequest,
5766
// The following comment can be used to register new unittest on new registration
5867
// new ObjectMapper().writeValueAsString(userFromProvider.getAttributes());
5968

60-
String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();
61-
62-
String keyForSub = switch (registrationId) {
63-
case "github":
64-
yield "id";
65-
default:
66-
throw new IllegalArgumentException("Unexpected value: " + registrationId);
67-
};
69+
KumiteUserRawRaw rawRaw = oauth2ToRawRaw(userFromProvider);
6870

69-
KumiteUserRawRaw rawRaw = KumiteUserRawRaw.builder()
70-
.providerId(registrationId)
71-
.sub(userFromProvider.getAttributes().get(keyForSub).toString())
72-
.build();
71+
String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();
72+
if (!registrationId.equals(rawRaw.getProviderId())) {
73+
throw new IllegalStateException("Inconsistent providerId inference from OAuth2User");
74+
}
7375

7476
String keyForPicture = switch (registrationId) {
75-
case "github":
77+
case PROVIDERID_GITHUB:
7678
yield "avatar_url";
79+
case PROVIDERID_GOOGLE:
80+
yield "picture";
7781
default:
7882
throw new IllegalArgumentException("Unexpected value: " + registrationId);
7983
};
@@ -97,4 +101,65 @@ private Mono<OAuth2User> processOAuth2User(OAuth2UserRequest oAuth2UserRequest,
97101

98102
}
99103

104+
public static KumiteUserRawRaw oauth2ToRawRaw(OAuth2User o) {
105+
String providerId = guessProviderId(o);
106+
String sub = getSub(providerId, o);
107+
KumiteUserRawRaw rawRaw = KumiteUserRawRaw.builder().providerId(providerId).sub(sub).build();
108+
return rawRaw;
109+
}
110+
111+
private static String getSub(String providerId, OAuth2User o) {
112+
if (PROVIDERID_GITHUB.equals(providerId)) {
113+
Object id = o.getAttribute("id");
114+
if (id == null) {
115+
throw new IllegalStateException("Invalid id: " + id);
116+
}
117+
return id.toString();
118+
} else if (PROVIDERID_GOOGLE.equals(providerId)) {
119+
Object sub = o.getAttribute("sub");
120+
if (sub == null) {
121+
throw new IllegalStateException("Invalid sub: " + sub);
122+
}
123+
return sub.toString();
124+
} else if (PROVIDERID_TEST.equals(providerId)) {
125+
Object id = o.getAttribute("id");
126+
if (id == null) {
127+
throw new IllegalStateException("Invalid sub: " + id);
128+
}
129+
return id.toString();
130+
} else {
131+
throw new IllegalStateException("Not managed providerId: " + providerId);
132+
}
133+
}
134+
135+
private static String guessProviderId(OAuth2User o) {
136+
if (isGoogle(o)) {
137+
return PROVIDERID_GOOGLE;
138+
} else if (PROVIDERID_TEST.equals(o.getAttribute("providerId"))) {
139+
return PROVIDERID_TEST;
140+
}
141+
return PROVIDERID_GITHUB;
142+
}
143+
144+
private static boolean isGoogle(OAuth2User o) {
145+
if (o.getAuthorities()
146+
.contains(new SimpleGrantedAuthority("SCOPE_https://www.googleapis.com/auth/userinfo.email"))) {
147+
return true;
148+
}
149+
150+
// TODO Unclear when we receive iss or not (Changed around introducing
151+
// SocialWebFluxSecurity.oidcReactiveOAuth2UserService)
152+
URL issuer = (URL) o.getAttribute("iss");
153+
if (issuer == null) {
154+
return false;
155+
}
156+
URI uri;
157+
try {
158+
uri = issuer.toURI();
159+
} catch (URISyntaxException e) {
160+
throw new IllegalStateException("Invalid iss: `%s`".formatted(issuer), e);
161+
}
162+
return "https://accounts.google.com".equals(uri.toASCIIString());
163+
}
164+
100165
}

server/src/main/resources/application-default_server.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ spring:
1010
clientId: ${kumite.login.oauth2.github.clientId:NEEDS_TO_BE_DEFINED}
1111
clientSecret: ${kumite.login.oauth2.github.clientSecret:NEEDS_TO_BE_DEFINED}
1212
google:
13-
client-id: google-client-id
14-
client-secret: google-client-secret
13+
client-id: ${kumite.login.oauth2.google.clientId:NEEDS_TO_BE_DEFINED}
14+
client-secret: ${kumite.login.oauth2.google.clientSecret:NEEDS_TO_BE_DEFINED}
1515
graphql:
1616
graphiql:
1717
enabled: true

server/src/main/resources/application-unsafe_external_oauth2.yml

+12-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33

44
kumite.login:
5-
# https://github.com/settings/applications/2686735
6-
oauth2.github.clientId: Ov23liakPa9YkyfEZGZE
7-
oauth2.github.clientSecret: 35faf73b496bb30fa17c1dd2f9fc8111874965ca
5+
# https://docs.spring.io/spring-security/site/docs/5.2.12.RELEASE/reference/html/oauth2.html
6+
# redirectUri: `{baseUrl}/login/oauth2/code/{registrationId}`
7+
oauth2:
8+
github:
9+
# https://github.com/settings/applications/2686735
10+
clientId: Ov23liakPa9YkyfEZGZE
11+
clientSecret: 35faf73b496bb30fa17c1dd2f9fc8111874965ca
12+
google:
13+
# https://developers.google.com/identity/sign-in/web/sign-in?hl=fr
14+
# https://console.cloud.google.com/apis/credentials?hl=fr&project=kumite
15+
clientId: 635294738113-sftufj1etk97bqdhj7m6949behc3clhn.apps.googleusercontent.com
16+
clientSecret: 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

server/src/main/resources/application.yml

-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ logging:
3333
level:
3434
org.springframework.security: INFO
3535
org.springframework.web.reactive: INFO
36-
eu.solven.kumite.app.webflux.KumiteExceptionRoutingWebFilter: DEBUG
3736
eu.solven.kumite.app.webflux.KumiteWebExceptionHandler: DEBUG
3837

3938
kumite.oauth2:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"at_hash":"Bh6lHMngzG6eaAWaew9XQw","sub":"106279441841850034209","email_verified":true,"iss":"https://accounts.google.com",
3+
"given_name":"Benoît","nonce":"c4gJrXkNzkL5vzW34qfo-NUkMU8L_Ki7KzL9myC3btw",
4+
"picture":"https://lh3.googleusercontent.com/a/ACg8ocI16JXR3Sv_fJeDYrbkLTNn5WRXSKBL6KL4T7U6SEYTXlOn9hPA=s96-c",
5+
"aud":["635294738113-sftufj1etk97bqdhj7m6949behc3clhn.apps.googleusercontent.com"],
6+
"azp":"635294738113-sftufj1etk97bqdhj7m6949behc3clhn.apps.googleusercontent.com","name":"Benoît Chatain Lacelle",
7+
"exp":"2024-09-24T17:20:00Z","family_name":"Chatain Lacelle","iat":"2024-09-24T16:20:00Z","email":"[email protected]"}

0 commit comments

Comments
 (0)