Skip to content

Commit

Permalink
Add OIDC login
Browse files Browse the repository at this point in the history
GitOrigin-RevId: 68500dc7deb1727c3a5504dd3212bd9222d4b0cf
  • Loading branch information
pdesgarets authored and Gitlab-CI committed Dec 4, 2024
1 parent 8562493 commit 22946d2
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
public enum LogAction {
USER_LOGIN_LTI,
USER_LOGIN_CAS,
USER_LOGIN_OIDC,
ENVIRONMENT_ASK,
ENVIRONMENT_CREATED_OK,
ENVIRONMENT_CREATED_KO,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import fr.centralesupelec.thuv.model.User;
import fr.centralesupelec.thuv.security.JwtTokenProvider;
import fr.centralesupelec.thuv.security.MyUserDetailsService;
import fr.centralesupelec.thuv.security.dtos.TokenOrigin;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -151,7 +152,7 @@ public ResponseEntity<String> launch(

String localToken = jwtTokenProvider
.generateToken(
user.getEmail()
user.getEmail(), TokenOrigin.LTI
);

activityLogger.log(LogAction.USER_LOGIN_LTI, user);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import fr.centralesupelec.thuv.security.dtos.TokenOrigin;
import io.sentry.Sentry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -32,7 +33,7 @@ public JwtTokenProvider(
verifier = JWT.require(algorithmHS256).build();
}

public String generateToken(String username) {
public String generateToken(String username, TokenOrigin tokenOrigin) {

Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
Expand All @@ -44,6 +45,7 @@ public String generateToken(String username) {
.withExpiresAt(expiryDate)
.withIssuedAt(now)
.withSubject(username)
.withClaim("origin", tokenOrigin.toString())
.sign(algorithmHS256);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package fr.centralesupelec.thuv.web;
package fr.centralesupelec.thuv.security.controllers;

import fr.centralesupelec.thuv.activity_logging.model.LogAction;
import fr.centralesupelec.thuv.activity_logging.services.ActivityLogger;
Expand All @@ -8,6 +8,7 @@
import fr.centralesupelec.thuv.model.cas.ServiceResponseType;
import fr.centralesupelec.thuv.security.JwtTokenProvider;
import fr.centralesupelec.thuv.security.MyUserDetailsService;
import fr.centralesupelec.thuv.security.dtos.TokenOrigin;
import io.sentry.Sentry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -125,7 +126,7 @@ public ResponseEntity<?> login(

String jwtToken = jwtTokenProvider
.generateToken(
user.getEmail()
user.getEmail(), TokenOrigin.CAS
);
logger.debug("JWT token: " + jwtToken);
activityLogger.log(LogAction.USER_LOGIN_CAS, user);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package fr.centralesupelec.thuv.security.controllers;

import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.JwkProviderBuilder;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.centralesupelec.thuv.activity_logging.model.LogAction;
import fr.centralesupelec.thuv.activity_logging.services.ActivityLogger;
import fr.centralesupelec.thuv.model.User;
import fr.centralesupelec.thuv.security.JwtTokenProvider;
import fr.centralesupelec.thuv.security.MyUserDetailsService;
import fr.centralesupelec.thuv.security.dtos.OIDCToken;
import fr.centralesupelec.thuv.security.dtos.TokenOrigin;
import io.sentry.Sentry;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.net.URL;
import java.security.interfaces.RSAPublicKey;
import java.util.List;
import java.util.concurrent.TimeUnit;

@RestController
@RequiredArgsConstructor
@RequestMapping("/auth/oidc")
public class OIDCController {
private static final Logger logger = LoggerFactory.getLogger(OIDCController.class);
private final MyUserDetailsService myUserDetailsService;
private final JwtTokenProvider jwtTokenProvider;
private final ActivityLogger activityLogger;
private final RestTemplate restTemplate;

@Value("#{'${oidc.allowed-audiences}'.split(',')}")
private List<String> allowedAudiences;
@Value("#{'${oidc.allowed-issuers}'.split(',')}")
private List<String> allowedIssuers;

private static final int JWKSCacheSize = 10;
private static final int JWKSCacheExpiresIn = 24;
private static final long AcceptedLeeway = 10L;

@PostMapping(value = "/")
public ResponseEntity<?> login(@RequestBody OIDCToken token) {
logger.debug("OIDC Token : " + token.getAccessToken());
DecodedJWT jwt = JWT.decode(token.getAccessToken());
try {
// TODO : Cache openid configuration
String responseBody = restTemplate.getForObject(
String.format("%s/%s", jwt.getIssuer(), ".well-known/openid-configuration"),
String.class
);
ObjectMapper objectMapper = new ObjectMapper();
String jwksUriValue = objectMapper.readTree(responseBody).get("jwks_uri").asText();

JwkProvider provider = new JwkProviderBuilder(new URL(jwksUriValue))
.cached(JWKSCacheSize, JWKSCacheExpiresIn, TimeUnit.HOURS)
.build();
logger.debug(String.format("Retrieving key of id '%s'", jwt.getKeyId()));
Jwk jwk = provider.get(jwt.getKeyId());
Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null);
JWTVerifier jwtVerifier = JWT.require(algorithm)
.acceptLeeway(AcceptedLeeway)
.withIssuer(this.allowedIssuers.toArray(new String[0]))
.withClaim("azp", (claim, decodedJWT) -> this.allowedAudiences.contains(claim.asString()))
.build();
jwtVerifier.verify(token.getAccessToken());

String email = jwt.getClaim("preferred_username").asString();
String name = jwt.getClaim("given_name").asString();
String lastName = jwt.getClaim("family_name").asString();

// FindorCreate user

User user = myUserDetailsService.upsertUser(email, name, lastName);
List<String> roles = (List<String>) jwt.getClaim("realm_access").asMap().get("roles");
if (roles.contains("teacher")) {
myUserDetailsService.ensureTeacher(user);
}

String jwtToken = jwtTokenProvider.generateToken(user.getEmail(), TokenOrigin.OIDC);
logger.debug("JWT token: " + jwtToken);
activityLogger.log(LogAction.USER_LOGIN_OIDC, user);
return ResponseEntity.ok(jwtToken);
} catch (Exception ex) {
Sentry.captureException(ex);
logger.error("An error occured during OIDC token verification " + ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("An error occured during OIDC token verification ... :(");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package fr.centralesupelec.thuv.security.dtos;

@lombok.Data
public class OIDCToken {
private String accessToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package fr.centralesupelec.thuv.security.dtos;

public enum TokenOrigin {
OIDC, CAS, LTI
}
4 changes: 4 additions & 0 deletions back/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,7 @@ spring.mustache.suffix=.html

deployment_delay_in_milliseconds=300000
deployment_enabled=true

# TODO: Set in provisioning
oidc.allowed-audiences=mydocker-local
oidc.allowed-issuers=https://keycloak.centralesupelec.fr/realms/mydocker-cs-preprod

0 comments on commit 22946d2

Please sign in to comment.