Skip to content

Commit

Permalink
Implement Okta OAuth authentication
Browse files Browse the repository at this point in the history
This approach depends on the access_token and not our ability to introspect it. The access token contains enough information to create a PIC-SURE user in our application.
  • Loading branch information
Gcolon021 committed Jan 24, 2024
1 parent f6be84a commit 01dcb9c
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,8 @@ public User createOpenAccessUser(Role openAccessRole) {
+ ", email: " + user.getEmail());
return user;
}

public createOktaUser() {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
import edu.harvard.dbmi.avillach.util.HttpClientUtil;
import edu.harvard.dbmi.avillach.util.response.PICSUREResponse;
import edu.harvard.hms.dbmi.avillach.auth.JAXRSConfiguration;
import edu.harvard.hms.dbmi.avillach.auth.data.entity.Connection;
import edu.harvard.hms.dbmi.avillach.auth.data.entity.Role;
import edu.harvard.hms.dbmi.avillach.auth.data.entity.User;
import edu.harvard.hms.dbmi.avillach.auth.data.repository.ConnectionRepository;
import edu.harvard.hms.dbmi.avillach.auth.data.repository.RoleRepository;
import edu.harvard.hms.dbmi.avillach.auth.data.repository.UserRepository;
import edu.harvard.hms.dbmi.avillach.auth.rest.UserService;
import edu.harvard.hms.dbmi.avillach.auth.utils.AuthUtils;
import edu.harvard.hms.dbmi.avillach.auth.utils.JWTUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.entity.StringEntity;
Expand All @@ -22,6 +27,9 @@
import javax.ws.rs.core.UriInfo;
import java.util.*;

/**
* This class handles the authentication process using Okta OAuth authentication.
*/
public class OktaOAuthAuthenticationService {

private final Logger logger = LoggerFactory.getLogger(this.getClass());
Expand All @@ -32,6 +40,9 @@ public class OktaOAuthAuthenticationService {
@Inject
private RoleRepository roleRepository;

@Inject
private ConnectionRepository connectionRepository;

@Inject
private AuthUtils authUtil;

Expand All @@ -47,22 +58,22 @@ public class OktaOAuthAuthenticationService {
public Response authenticate(UriInfo uriInfo, Map<String, String> authRequest) {
String code = authRequest.get("code");
if (StringUtils.isNotBlank(code)) {

JsonNode userToken = handleCodeTokenExchange(uriInfo, code);
logger.info("UserToken: " + userToken);
JsonNode introspectResponse = introspectToken(userToken);

if (introspectResponse == null) {
return PICSUREResponse.error("Failed to introspect access token.");
}

logger.info("Introspection Token: " + introspectResponse);
Jws<Claims> oktaPayloadClaims = oktaAuthorizationTokenToClaims(userToken);
logger.info("oktaPayloadClaims: " + oktaPayloadClaims);

User user = initializeUser(introspectResponse);
if (user == null) {
logger.info("LOGIN FAILED ___ USER NOT FOUND ___ " + userToken.get("email").asText() + ":" + userToken.get("sub").asText() + " ___");
return PICSUREResponse.error("User not found");
Map<String, Object> oktaPayloadClaimsMap = convertOktaPayloadClaimsToMap(oktaPayloadClaims);
if (oktaPayloadClaimsMap == null) {
logger.info("LOGIN FAILED ___ USER FAILED TO AUTHENTICATE ___");
return PICSUREResponse.error("User not authenticated");
}

logger.info("oktaPayloadClaimsMap: " + oktaPayloadClaimsMap);
User user = initializeUser(oktaPayloadClaimsMap);

HashMap<String, String> responseMap = createUserClaims(user);
logger.info("LOGIN SUCCESS ___ " + user.getEmail() + ":" + user.getUuid().toString() + " ___ Authorization will expire at ___ " + responseMap.get("expirationDate") + "___");

Expand All @@ -73,19 +84,73 @@ public Response authenticate(UriInfo uriInfo, Map<String, String> authRequest) {
return PICSUREResponse.error("User not authenticated");
}

private User initializeUser(JsonNode introspectResponse) {
boolean isActive = introspectResponse.get("active").asBoolean();
if (!isActive) {
logger.info("LOGIN FAILED ___ USER IS NOT ACTIVE ___ ");
/**
* Convert the claims from the OKTA payload to a map.
* Example Payload Claims:
* {
* "ver": 1,
* "jti": "AT.N6-700lOHdD0VHMs7r94DR9kHUIGKgLyIh_zk94OFOk",
* "iss": "https://hms-srce.oktapreview.com",
* "aud": "https://hms-srce.oktapreview.com",
* "sub": "[email protected]",
* "iat": 1706049726,
* "exp": 1706053326,
* "cid": "0oacgzw1kdNsPgRw11d7",
* "uid": "00ubtr4ospshhg12r1d7",
* "scp": [
* "openid"
* ],
* "auth_time": 1706042809
* }
*
* @param oktaPayloadClaims The claims from the OKTA payload
* @return The claims as a map
*/
private Map<String, Object> convertOktaPayloadClaimsToMap(Jws<Claims> oktaPayloadClaims) {
if (oktaPayloadClaims == null) {
return null;
}

Claims body = oktaPayloadClaims.getBody();
Map<String, Object> oktaPayloadClaimsMap = new HashMap<>();
oktaPayloadClaimsMap.put("sub", body.get("sub"));
oktaPayloadClaimsMap.put("ver", body.get("ver"));
oktaPayloadClaimsMap.put("jti", body.get("jti"));
oktaPayloadClaimsMap.put("iss", body.get("iss"));
oktaPayloadClaimsMap.put("aud", body.get("aud"));
oktaPayloadClaimsMap.put("iat", body.get("iat"));
oktaPayloadClaimsMap.put("exp", body.get("exp"));
oktaPayloadClaimsMap.put("cid", body.get("cid"));
oktaPayloadClaimsMap.put("uid", body.get("uid"));
oktaPayloadClaimsMap.put("scp", body.get("scp"));

return oktaPayloadClaimsMap;
}


private Jws<Claims> oktaAuthorizationTokenToClaims(JsonNode userToken) {
if (userToken.has("access_token")) {
logger.info("LOGIN FAILED ___ USER FAILED TO AUTHENTICATE ___");
return null;
}
JsonNode accessToken = userToken.get("access_token");
return JWTUtil.parseToken(JAXRSConfiguration.spClientSecret, accessToken.asText());
}

User user = loadUser(introspectResponse);
private User initializeUser(Map<String, Object> oktaPayloadClaimsMap) {
User user = loadUser(oktaPayloadClaimsMap);
clearCache(user);
user = addUserRoles(user);
addUserRoles(user);

return user;
}

/**
* Create the claims for the user. This will be used to create the user session for the UI client.
*
* @param user The user
* @return The claims for the user
*/
private HashMap<String, String> createUserClaims(User user) {
HashMap<String, Object> claims = new HashMap<>();
claims.put("name", user.getName());
Expand All @@ -95,9 +160,10 @@ private HashMap<String, String> createUserClaims(User user) {
}


private User addUserRoles(User user) {
private void addUserRoles(User user) {
Role openAccessRole = roleRepository.getUniqueResultByColumn("name", FENCEAuthenticationService.fence_open_access_role_name);
return userRepository.createOpenAccessUser(openAccessRole);
user.setRoles(new HashSet<>(List.of(openAccessRole)));
userRepository.merge(user);
}

private void clearCache(User user) {
Expand All @@ -110,19 +176,22 @@ private void clearCache(User user) {
* will reject their login attempt.
* Documentation: <a href="https://developer.okta.com/docs/reference/api/oidc/#response-example-success-access-token">response-example-success-access-token</a>
*
* @param introspectResponse The response from the introspect endpoint
* @param oktaPayloadClaimsMap The response from the introspect endpoint
* @return The user
*/
private User loadUser(JsonNode introspectResponse) {
String email = introspectResponse.get("username").asText();
// TODO: Load the user from the database. For now, just return a new user so we can test.
private User loadUser(Map<String, Object> oktaPayloadClaimsMap) {
String email = (String) oktaPayloadClaimsMap.get("sub");
long userId = (long) oktaPayloadClaimsMap.get("uid");
logger.info("loadUser() - email: " + email + ", userId: " + userId);

Connection okta = connectionRepository.findConnectionById("OKTA");
User user = new User();
user.setSubject(introspectResponse.get("sub").asText());
user.setSubject("okta|" + userId);
user.setEmail(email);
user.setConnection(null); // TODO: We need to load the connection from the database.
user.setConnection(okta);
user.setAcceptedTOS(new Date());
user.setGeneralMetadata(introspectResponse.toString());
user.setActive(introspectResponse.get("active").asBoolean());
user.setGeneralMetadata(JAXRSConfiguration.objectMapper.convertValue(oktaPayloadClaimsMap, JsonNode.class).toString());
user.setActive(true);

return user;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package edu.harvard.hms.dbmi.avillach.auth.utils;

import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.Key;
import java.util.Date;
import java.util.Map;
Expand Down Expand Up @@ -76,4 +75,22 @@ public static String createJwtToken(String clientSecret, String id, String issue

return jwt_token;
}

public static Jws<Claims> parseToken(String spClientSecret, String text) {
try {
return Jwts.parser().setSigningKey(spClientSecret.getBytes()).parseClaimsJws(text);
} catch (SignatureException e) {
try {
return Jwts.parser().setSigningKey(spClientSecret.getBytes("UTF-8")).parseClaimsJws(text);
} catch (UnsupportedEncodingException ex){
logger.error("parseToken() clientSecret encoding UTF-8 is not supported. "
+ ex.getClass().getSimpleName() + ": " + ex.getMessage());
} catch (JwtException | IllegalArgumentException ex) {
logger.error("parseToken() throws: " + e.getClass().getSimpleName() + ", " + e.getMessage());
}
} catch (JwtException | IllegalArgumentException e) {
logger.error("parseToken() throws: " + e.getClass().getSimpleName() + ", " + e.getMessage());
}
return null;
}
}

0 comments on commit 01dcb9c

Please sign in to comment.