From c457025a6857a488063969510c3395160e31d226 Mon Sep 17 00:00:00 2001 From: Jonas Hein Date: Fri, 23 Feb 2024 16:04:11 +0100 Subject: [PATCH 1/2] Added Ocid unilogin solution first draft --- .../RedirectingAuthenticationProvider.java | 8 +- .../dk/acto/fafnir/sso/conf/BeanConf.java | 1 + .../provider/UniLoginLightweightProvider.java | 111 +++++++++++++++--- .../sso/provider/unilogin/AccessToken.java | 20 ++++ .../provider/unilogin/IntrospectionToken.java | 29 +++++ .../unilogin/UniloginTokenCredentials.java | 13 ++ .../sso/service/SsoProviderService.java | 33 ++++-- .../UniLoginLightweightController.java | 15 ++- .../dk/acto/fafnir/sso/util/PkceUtil.java | 28 +++++ 9 files changed, 220 insertions(+), 38 deletions(-) create mode 100644 sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/AccessToken.java create mode 100644 sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/IntrospectionToken.java create mode 100644 sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UniloginTokenCredentials.java create mode 100644 sso/src/main/java/dk/acto/fafnir/sso/util/PkceUtil.java diff --git a/client/src/main/java/dk/acto/fafnir/api/provider/RedirectingAuthenticationProvider.java b/client/src/main/java/dk/acto/fafnir/api/provider/RedirectingAuthenticationProvider.java index 5db9ef7..105396e 100644 --- a/client/src/main/java/dk/acto/fafnir/api/provider/RedirectingAuthenticationProvider.java +++ b/client/src/main/java/dk/acto/fafnir/api/provider/RedirectingAuthenticationProvider.java @@ -1,10 +1,14 @@ package dk.acto.fafnir.api.provider; +import com.hazelcast.security.TokenCredentials; import dk.acto.fafnir.api.model.AuthenticationResult; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; + public interface RedirectingAuthenticationProvider extends ProviderInformation { - String authenticate(); + String authenticate() throws NoSuchAlgorithmException; - AuthenticationResult callback(T data); + AuthenticationResult callback(T data) throws IOException; } diff --git a/sso/src/main/java/dk/acto/fafnir/sso/conf/BeanConf.java b/sso/src/main/java/dk/acto/fafnir/sso/conf/BeanConf.java index a4fe19b..2e59127 100644 --- a/sso/src/main/java/dk/acto/fafnir/sso/conf/BeanConf.java +++ b/sso/src/main/java/dk/acto/fafnir/sso/conf/BeanConf.java @@ -21,6 +21,7 @@ import dk.acto.fafnir.sso.service.MicrosoftIdentityApi; import dk.acto.fafnir.sso.service.MitIdApi; import dk.acto.fafnir.sso.service.UniLoginApi; +import dk.acto.fafnir.sso.util.PkceUtil; import dk.acto.fafnir.sso.util.TokenFactory; import dk.acto.fafnir.sso.util.generator.DemoDataGenerator; import io.vavr.control.Try; diff --git a/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginLightweightProvider.java b/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginLightweightProvider.java index 22afed8..828205b 100644 --- a/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginLightweightProvider.java +++ b/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginLightweightProvider.java @@ -1,7 +1,6 @@ package dk.acto.fafnir.sso.provider; -import com.auth0.jwt.JWT; -import com.auth0.jwt.interfaces.Claim; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.scribejava.core.oauth.OAuth20Service; import dk.acto.fafnir.api.exception.ProviderAttributeMissing; import dk.acto.fafnir.api.model.*; @@ -9,43 +8,83 @@ import dk.acto.fafnir.api.provider.metadata.MetadataProvider; import dk.acto.fafnir.sso.model.conf.ProviderConf; import dk.acto.fafnir.sso.provider.credentials.TokenCredentials; +import dk.acto.fafnir.sso.provider.unilogin.AccessToken; +import dk.acto.fafnir.sso.provider.unilogin.IntrospectionToken; +import dk.acto.fafnir.sso.provider.unilogin.UniloginTokenCredentials; +import dk.acto.fafnir.sso.util.PkceUtil; import dk.acto.fafnir.sso.util.TokenFactory; -import io.vavr.control.Try; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; -import java.util.Map; +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; @Slf4j @AllArgsConstructor -public class UniLoginLightweightProvider implements RedirectingAuthenticationProvider { +public class UniLoginLightweightProvider implements RedirectingAuthenticationProvider { private final OAuth20Service uniloginOauth; private final TokenFactory tokenFactory; private final ProviderConf providerConf; @Override - public String authenticate() { - return uniloginOauth.getAuthorizationUrl(Map.of("response_mode", "form_post")); + public String authenticate() throws NoSuchAlgorithmException { + String responseType = "response_type=" + URLEncoder.encode("code"); + String client = "&client_id=" + URLEncoder.encode("http://localhost:8080/"); + String redirect = "&redirect_uri=" + URLEncoder.encode("http://localhost:8080/unilogin-lightweight/callback"); + String codeChallengeMethod = "&code_challenge_method=" + URLEncoder.encode("S256"); + String codeVerifier = PkceUtil.generateCodeVerifier(); + String codeChallenge = "&code_challenge=" + URLEncoder.encode(PkceUtil.generateCodeChallenge(codeVerifier)); + String nonce = "&nonce=" + URLEncoder.encode(new SecureRandom().ints(16, 0, 256) + .mapToObj(i -> String.format("%02x", i)) + .collect(Collectors.joining())); + String state = "&state=" + URLEncoder.encode(codeVerifier); + String scope = "&scope=" + URLEncoder.encode("openid"); + String responseMode = "&response_mode=" + URLEncoder.encode("form_post"); + return "https://et-broker.unilogin.dk/auth/realms/broker/protocol/openid-connect" + "/auth?" + responseType + client + redirect + codeChallengeMethod + codeChallenge + nonce + state + scope + responseMode; } @Override - public AuthenticationResult callback(TokenCredentials data) { - var token = Try.of(() -> JWT.decode(data.getCode())) - .onFailure(x -> log.error("Authentication failed", x)) - .getOrNull(); + public AuthenticationResult callback(UniloginTokenCredentials data) throws IOException { + var UL_CLIENT_ID = System.getenv("UL_CLIENT_ID"); + var UL_SECRET = System.getenv("UL_SECRET"); + var UL_REDIRECT_URL = System.getenv("UL_REDIRECT_URL"); + var CODE_VERIFIER = data.getState(); + var OID_BASE_URL = "https://et-broker.unilogin.dk/auth/realms/broker/protocol/openid-connect/"; - if (token == null) { - return AuthenticationResult.failure(FailureReason.AUTHENTICATION_FAILED); - } - var subject = Optional.ofNullable(token.getClaim("sub")) - .map(Claim::asString) + var accessCode = data.getCode(); + AccessToken accessToken; + + accessToken = getAccessToken(accessCode, UL_CLIENT_ID, UL_SECRET, UL_REDIRECT_URL, CODE_VERIFIER, OID_BASE_URL); + + IntrospectionToken intro; + + intro = getIntrospectToken(accessToken.getAccess_token(),UL_CLIENT_ID,UL_SECRET,OID_BASE_URL); + + + if (intro == null) { + return AuthenticationResult.failure(FailureReason.AUTHENTICATION_FAILED); + } + + var subject = Optional.ofNullable(intro.getSub()) .map(providerConf::applySubjectRules) .orElseThrow(ProviderAttributeMissing::new); - var displayName = Optional.ofNullable(token.getClaim("customDisplayNameClaim")) - .map(Claim::asString) + var displayName = Optional.of("") .orElseThrow(ProviderAttributeMissing::new); var subjectActual = UserData.builder() @@ -60,6 +99,42 @@ public AuthenticationResult callback(TokenCredentials data) { return AuthenticationResult.success(jwt); } + private AccessToken getAccessToken(String code, String clientId, String clientSecret, String redirectUri, String codeVerifier, String oidcBaseUrl) throws IOException { + List nvps = new ArrayList<>(); + nvps.add(new BasicNameValuePair("grant_type", "authorization_code")); + nvps.add(new BasicNameValuePair("code", code)); + nvps.add(new BasicNameValuePair("client_id", clientId)); + nvps.add(new BasicNameValuePair("client_secret", clientSecret)); + nvps.add(new BasicNameValuePair("redirect_uri", redirectUri)); + nvps.add(new BasicNameValuePair("code_verifier", codeVerifier)); + HttpResponse response = httpPostRequest(oidcBaseUrl + "/token", nvps); + + return getObjectMapper().readValue(response.getEntity().getContent(), AccessToken.class); + } + + private IntrospectionToken getIntrospectToken(String accesstoken,String clientId,String clientSecret, String oidcBaseUrl) throws IOException { + List nvps = new ArrayList<>(); + nvps.add(new BasicNameValuePair("token", accesstoken)); + nvps.add(new BasicNameValuePair("client_id", clientId)); + nvps.add(new BasicNameValuePair("client_secret", clientSecret)); + HttpResponse response = httpPostRequest(oidcBaseUrl + "/token/introspect", nvps); + + return getObjectMapper().readValue(response.getEntity().getContent(), IntrospectionToken.class); + } + + private HttpResponse httpPostRequest(String uri, List nvps) throws IOException { + CloseableHttpClient httpClient = HttpClients.createDefault(); + HttpPost httpPost = new HttpPost(); + httpPost.setURI(URI.create(uri)); + httpPost.setEntity(new UrlEncodedFormEntity(nvps)); + HttpResponse response = httpClient.execute(httpPost); + return response; + } + + ObjectMapper getObjectMapper() { + return new ObjectMapper(); + } + @Override public ProviderMetaData getMetaData() { return MetadataProvider.UNILOGIN; diff --git a/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/AccessToken.java b/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/AccessToken.java new file mode 100644 index 0000000..7a07df1 --- /dev/null +++ b/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/AccessToken.java @@ -0,0 +1,20 @@ +package dk.acto.fafnir.sso.provider.unilogin; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class AccessToken { + String access_token; + int expires_in; + int refresh_expires_in; + String refresh_token; + String token_type; + String id_token; + String nonce; + String session_state; + String scope; +} diff --git a/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/IntrospectionToken.java b/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/IntrospectionToken.java new file mode 100644 index 0000000..e95625f --- /dev/null +++ b/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/IntrospectionToken.java @@ -0,0 +1,29 @@ +package dk.acto.fafnir.sso.provider.unilogin; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class IntrospectionToken { + int exp; + int iat; + int auth_time; + String jti; + String iss; + String sub; + String typ; + String azp; + String session_state; + String nonce; + String acr; + String scope; + String spec_ver; + String unilogin_loa; + String aktoer_gruppe; + String loa; + String uniid; + String client_id; +} diff --git a/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UniloginTokenCredentials.java b/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UniloginTokenCredentials.java new file mode 100644 index 0000000..99a6e55 --- /dev/null +++ b/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UniloginTokenCredentials.java @@ -0,0 +1,13 @@ +package dk.acto.fafnir.sso.provider.unilogin; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +@AllArgsConstructor +public class UniloginTokenCredentials { + String code; + String state; +} diff --git a/sso/src/main/java/dk/acto/fafnir/sso/service/SsoProviderService.java b/sso/src/main/java/dk/acto/fafnir/sso/service/SsoProviderService.java index 369ba73..4818993 100644 --- a/sso/src/main/java/dk/acto/fafnir/sso/service/SsoProviderService.java +++ b/sso/src/main/java/dk/acto/fafnir/sso/service/SsoProviderService.java @@ -8,6 +8,7 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; +import java.security.NoSuchAlgorithmException; import java.util.Set; @AllArgsConstructor @@ -18,28 +19,34 @@ public class SsoProviderService implements ProviderService { @Override public String[] getAcceptedProviders() { return providerInformationSet.stream() - .map(ProviderInformation::getMetaData) - .map(ProviderMetaData::getProviderId) - .toArray(String[]::new); + .map(ProviderInformation::getMetaData) + .map(ProviderMetaData::getProviderId) + .toArray(String[]::new); } @Override public ProviderMetaData getProviderMetaData(String providerId) { return providerInformationSet.stream() - .map(ProviderInformation::getMetaData) - .filter(metaData -> metaData.getProviderId().equals(providerId)) - .findAny() - .orElseThrow(NoSuchProvider::new); + .map(ProviderInformation::getMetaData) + .filter(metaData -> metaData.getProviderId().equals(providerId)) + .findAny() + .orElseThrow(NoSuchProvider::new); } @Override public String getAuthenticationUrlForProvider(String providerId) { return providerInformationSet.stream() - .filter(pi -> pi.getMetaData().getProviderId().equals(providerId)) - .filter(RedirectingAuthenticationProvider.class::isInstance) - .map(RedirectingAuthenticationProvider.class::cast) - .map(RedirectingAuthenticationProvider::authenticate) - .findFirst() - .orElseThrow(NoSuchProvider::new); + .filter(pi -> pi.getMetaData().getProviderId().equals(providerId)) + .filter(RedirectingAuthenticationProvider.class::isInstance) + .map(RedirectingAuthenticationProvider.class::cast) + .map(provider -> { + try { + return provider.authenticate(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Failed to generate authentication URL due to missing algorithm.", e); + } + }) + .findFirst() + .orElseThrow(NoSuchProvider::new); } } diff --git a/sso/src/main/java/dk/acto/fafnir/sso/service/controller/UniLoginLightweightController.java b/sso/src/main/java/dk/acto/fafnir/sso/service/controller/UniLoginLightweightController.java index d19906c..7ea0934 100644 --- a/sso/src/main/java/dk/acto/fafnir/sso/service/controller/UniLoginLightweightController.java +++ b/sso/src/main/java/dk/acto/fafnir/sso/service/controller/UniLoginLightweightController.java @@ -2,7 +2,7 @@ import dk.acto.fafnir.api.model.conf.FafnirConf; import dk.acto.fafnir.sso.provider.UniLoginLightweightProvider; -import dk.acto.fafnir.sso.provider.credentials.TokenCredentials; +import dk.acto.fafnir.sso.provider.unilogin.UniloginTokenCredentials; import jakarta.annotation.PostConstruct; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -14,6 +14,9 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.view.RedirectView; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; + @Controller @Slf4j @AllArgsConstructor @@ -24,15 +27,17 @@ public class UniLoginLightweightController { private final FafnirConf uniloginConf; @GetMapping - public RedirectView authenticate() { + public RedirectView authenticate() throws NoSuchAlgorithmException { return new RedirectView(provider.authenticate()); } @PostMapping("callback") - public RedirectView callback(@RequestParam("id_token") String code) { - return new RedirectView(provider.callback(TokenCredentials.builder() + public RedirectView callback(@RequestParam("code") String code, @RequestParam("state") String state) throws IOException { + return new RedirectView(provider.callback(UniloginTokenCredentials.builder() .code(code) - .build()).getUrl(uniloginConf)); + .state(state) + .build() + ).getUrl(uniloginConf)); } @PostConstruct diff --git a/sso/src/main/java/dk/acto/fafnir/sso/util/PkceUtil.java b/sso/src/main/java/dk/acto/fafnir/sso/util/PkceUtil.java new file mode 100644 index 0000000..50c050c --- /dev/null +++ b/sso/src/main/java/dk/acto/fafnir/sso/util/PkceUtil.java @@ -0,0 +1,28 @@ +package dk.acto.fafnir.sso.util; + +import lombok.SneakyThrows; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; + +public class PkceUtil { + + public static String generateCodeVerifier() { + SecureRandom sr = new SecureRandom(); + byte[] codeVerifier = new byte[32]; + sr.nextBytes(codeVerifier); + return Base64.getUrlEncoder().withoutPadding().encodeToString(codeVerifier); + } + + + public static String generateCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException { + byte[] bytes = codeVerifier.getBytes(); + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(bytes, 0, bytes.length); + byte[] digest = md.digest(); + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + } +} + From 2087b64d4d346d10a8f24d5cf868fc8e74edb7a6 Mon Sep 17 00:00:00 2001 From: Jonas Hein Date: Tue, 27 Feb 2024 16:27:49 +0100 Subject: [PATCH 2/2] Finished Unilogin with OIDC now codeVerifier is not stored in session --- .../RedirectingAuthenticationProvider.java | 1 + ...directingUnilogAuthenticationProvider.java | 15 ++++++ sso/bootstrap.sh | 0 .../dk/acto/fafnir/sso/conf/BeanConf.java | 5 +- .../provider/UniLoginLightweightProvider.java | 49 ++++++++++--------- .../unilogin/UniloginTokenCredentials.java | 1 - .../UniLoginLightweightController.java | 15 +++--- 7 files changed, 51 insertions(+), 35 deletions(-) create mode 100644 client/src/main/java/dk/acto/fafnir/api/provider/RedirectingUnilogAuthenticationProvider.java mode change 100755 => 100644 sso/bootstrap.sh diff --git a/client/src/main/java/dk/acto/fafnir/api/provider/RedirectingAuthenticationProvider.java b/client/src/main/java/dk/acto/fafnir/api/provider/RedirectingAuthenticationProvider.java index 105396e..ee2bca4 100644 --- a/client/src/main/java/dk/acto/fafnir/api/provider/RedirectingAuthenticationProvider.java +++ b/client/src/main/java/dk/acto/fafnir/api/provider/RedirectingAuthenticationProvider.java @@ -3,6 +3,7 @@ import com.hazelcast.security.TokenCredentials; import dk.acto.fafnir.api.model.AuthenticationResult; +import jakarta.servlet.http.HttpSession; import java.io.IOException; import java.security.NoSuchAlgorithmException; diff --git a/client/src/main/java/dk/acto/fafnir/api/provider/RedirectingUnilogAuthenticationProvider.java b/client/src/main/java/dk/acto/fafnir/api/provider/RedirectingUnilogAuthenticationProvider.java new file mode 100644 index 0000000..0b7cc9c --- /dev/null +++ b/client/src/main/java/dk/acto/fafnir/api/provider/RedirectingUnilogAuthenticationProvider.java @@ -0,0 +1,15 @@ +package dk.acto.fafnir.api.provider; + +import dk.acto.fafnir.api.model.AuthenticationResult; +import jakarta.servlet.http.HttpSession; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; + + +public interface RedirectingUnilogAuthenticationProvider extends ProviderInformation { + + String authenticate(HttpSession session) throws NoSuchAlgorithmException; + + AuthenticationResult callback(T data, HttpSession session) throws IOException; +} diff --git a/sso/bootstrap.sh b/sso/bootstrap.sh old mode 100755 new mode 100644 diff --git a/sso/src/main/java/dk/acto/fafnir/sso/conf/BeanConf.java b/sso/src/main/java/dk/acto/fafnir/sso/conf/BeanConf.java index 2e59127..ffa6acc 100644 --- a/sso/src/main/java/dk/acto/fafnir/sso/conf/BeanConf.java +++ b/sso/src/main/java/dk/acto/fafnir/sso/conf/BeanConf.java @@ -21,7 +21,6 @@ import dk.acto.fafnir.sso.service.MicrosoftIdentityApi; import dk.acto.fafnir.sso.service.MitIdApi; import dk.acto.fafnir.sso.service.UniLoginApi; -import dk.acto.fafnir.sso.util.PkceUtil; import dk.acto.fafnir.sso.util.TokenFactory; import dk.acto.fafnir.sso.util.generator.DemoDataGenerator; import io.vavr.control.Try; @@ -137,7 +136,7 @@ public UniLoginProvider uniLoginProvider( @Bean - @ConditionalOnProperty(name = {"UL_CLIENT_ID", "UL_SECRET"}) + @ConditionalOnProperty(name = {"UL_CLIENT_ID", "UL_SECRET", "FAFNIR_URL"}) public UniLoginLightweightProvider uniLoginLightweightProvider( @Value("${UL_CLIENT_ID}") final String appId, @Value("${UL_SECRET}") final String secret, @@ -151,7 +150,7 @@ public UniLoginLightweightProvider uniLoginLightweightProvider( .callback(fafnirConf.getUrl() + "/unilogin-lightweight/callback") .defaultScope("openid") .build(new UniLoginApi())) - .map(oAuth20Service -> new UniLoginLightweightProvider(oAuth20Service, tokenFactory, providerConf)) + .map(oAuth20Service -> new UniLoginLightweightProvider(tokenFactory, providerConf)) .toJavaOptional() .orElseThrow(UniloginLightweightConfigurationBroken::new); } diff --git a/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginLightweightProvider.java b/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginLightweightProvider.java index 828205b..58b7d6c 100644 --- a/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginLightweightProvider.java +++ b/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginLightweightProvider.java @@ -1,18 +1,17 @@ package dk.acto.fafnir.sso.provider; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.scribejava.core.oauth.OAuth20Service; import dk.acto.fafnir.api.exception.ProviderAttributeMissing; import dk.acto.fafnir.api.model.*; -import dk.acto.fafnir.api.provider.RedirectingAuthenticationProvider; +import dk.acto.fafnir.api.provider.RedirectingUnilogAuthenticationProvider; import dk.acto.fafnir.api.provider.metadata.MetadataProvider; import dk.acto.fafnir.sso.model.conf.ProviderConf; -import dk.acto.fafnir.sso.provider.credentials.TokenCredentials; import dk.acto.fafnir.sso.provider.unilogin.AccessToken; import dk.acto.fafnir.sso.provider.unilogin.IntrospectionToken; import dk.acto.fafnir.sso.provider.unilogin.UniloginTokenCredentials; import dk.acto.fafnir.sso.util.PkceUtil; import dk.acto.fafnir.sso.util.TokenFactory; +import jakarta.servlet.http.HttpSession; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpResponse; @@ -35,36 +34,40 @@ @Slf4j @AllArgsConstructor -public class UniLoginLightweightProvider implements RedirectingAuthenticationProvider { - private final OAuth20Service uniloginOauth; +public class UniLoginLightweightProvider implements RedirectingUnilogAuthenticationProvider { private final TokenFactory tokenFactory; private final ProviderConf providerConf; @Override - public String authenticate() throws NoSuchAlgorithmException { - String responseType = "response_type=" + URLEncoder.encode("code"); - String client = "&client_id=" + URLEncoder.encode("http://localhost:8080/"); - String redirect = "&redirect_uri=" + URLEncoder.encode("http://localhost:8080/unilogin-lightweight/callback"); - String codeChallengeMethod = "&code_challenge_method=" + URLEncoder.encode("S256"); - String codeVerifier = PkceUtil.generateCodeVerifier(); - String codeChallenge = "&code_challenge=" + URLEncoder.encode(PkceUtil.generateCodeChallenge(codeVerifier)); - String nonce = "&nonce=" + URLEncoder.encode(new SecureRandom().ints(16, 0, 256) + public String authenticate(HttpSession session) throws NoSuchAlgorithmException { + var codeVerifier = PkceUtil.generateCodeVerifier(); + session.setAttribute("codeVerifier", codeVerifier); + + var responseType = "response_type=" + URLEncoder.encode("code"); + var client = "&client_id=" + URLEncoder.encode(System.getenv("UL_CLIENT_ID")); + var redirect = "&redirect_uri=" + URLEncoder.encode(System.getenv("FAFNIR_URL") + "/unilogin-lightweight/callback"); + var codeChallengeMethod = "&code_challenge_method=" + URLEncoder.encode("S256"); + var codeChallenge = "&code_challenge=" + URLEncoder.encode(PkceUtil.generateCodeChallenge(codeVerifier)); + var nonce = "&nonce=" + URLEncoder.encode(new SecureRandom().ints(16, 0, 256) .mapToObj(i -> String.format("%02x", i)) .collect(Collectors.joining())); - String state = "&state=" + URLEncoder.encode(codeVerifier); - String scope = "&scope=" + URLEncoder.encode("openid"); - String responseMode = "&response_mode=" + URLEncoder.encode("form_post"); + var state = "&state=" + URLEncoder.encode(new SecureRandom().ints(16, 0, 256) + .mapToObj(i -> String.format("%02x", i)) + .collect(Collectors.joining())); + var scope = "&scope=" + URLEncoder.encode("openid"); + var responseMode = "&response_mode=" + URLEncoder.encode("form_post"); return "https://et-broker.unilogin.dk/auth/realms/broker/protocol/openid-connect" + "/auth?" + responseType + client + redirect + codeChallengeMethod + codeChallenge + nonce + state + scope + responseMode; } @Override - public AuthenticationResult callback(UniloginTokenCredentials data) throws IOException { + public AuthenticationResult callback(UniloginTokenCredentials data, HttpSession session) throws IOException { var UL_CLIENT_ID = System.getenv("UL_CLIENT_ID"); var UL_SECRET = System.getenv("UL_SECRET"); - var UL_REDIRECT_URL = System.getenv("UL_REDIRECT_URL"); - var CODE_VERIFIER = data.getState(); + var UL_REDIRECT_URL = System.getenv("FAFNIR_URL") + "/unilogin-lightweight/callback"; var OID_BASE_URL = "https://et-broker.unilogin.dk/auth/realms/broker/protocol/openid-connect/"; + var CODE_VERIFIER = (String) session.getAttribute("codeVerifier"); + var accessCode = data.getCode(); AccessToken accessToken; @@ -73,12 +76,12 @@ public AuthenticationResult callback(UniloginTokenCredentials data) throws IOExc IntrospectionToken intro; - intro = getIntrospectToken(accessToken.getAccess_token(),UL_CLIENT_ID,UL_SECRET,OID_BASE_URL); + intro = getIntrospectToken(accessToken.getAccess_token(), UL_CLIENT_ID, UL_SECRET, OID_BASE_URL); if (intro == null) { - return AuthenticationResult.failure(FailureReason.AUTHENTICATION_FAILED); - } + return AuthenticationResult.failure(FailureReason.AUTHENTICATION_FAILED); + } var subject = Optional.ofNullable(intro.getSub()) .map(providerConf::applySubjectRules) @@ -112,7 +115,7 @@ private AccessToken getAccessToken(String code, String clientId, String clientSe return getObjectMapper().readValue(response.getEntity().getContent(), AccessToken.class); } - private IntrospectionToken getIntrospectToken(String accesstoken,String clientId,String clientSecret, String oidcBaseUrl) throws IOException { + private IntrospectionToken getIntrospectToken(String accesstoken, String clientId, String clientSecret, String oidcBaseUrl) throws IOException { List nvps = new ArrayList<>(); nvps.add(new BasicNameValuePair("token", accesstoken)); nvps.add(new BasicNameValuePair("client_id", clientId)); diff --git a/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UniloginTokenCredentials.java b/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UniloginTokenCredentials.java index 99a6e55..65a2248 100644 --- a/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UniloginTokenCredentials.java +++ b/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UniloginTokenCredentials.java @@ -9,5 +9,4 @@ @AllArgsConstructor public class UniloginTokenCredentials { String code; - String state; } diff --git a/sso/src/main/java/dk/acto/fafnir/sso/service/controller/UniLoginLightweightController.java b/sso/src/main/java/dk/acto/fafnir/sso/service/controller/UniLoginLightweightController.java index 7ea0934..1d5593b 100644 --- a/sso/src/main/java/dk/acto/fafnir/sso/service/controller/UniLoginLightweightController.java +++ b/sso/src/main/java/dk/acto/fafnir/sso/service/controller/UniLoginLightweightController.java @@ -4,6 +4,7 @@ import dk.acto.fafnir.sso.provider.UniLoginLightweightProvider; import dk.acto.fafnir.sso.provider.unilogin.UniloginTokenCredentials; import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpSession; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -21,27 +22,25 @@ @Slf4j @AllArgsConstructor @RequestMapping("unilogin-lightweight") -@ConditionalOnProperty(name = {"UL_CLIENT_ID", "UL_SECRET"}) +@ConditionalOnProperty(name = {"UL_CLIENT_ID", "UL_SECRET", "FAFNIR_URL"}) public class UniLoginLightweightController { private final UniLoginLightweightProvider provider; private final FafnirConf uniloginConf; @GetMapping - public RedirectView authenticate() throws NoSuchAlgorithmException { - return new RedirectView(provider.authenticate()); + public RedirectView authenticate(HttpSession session) throws NoSuchAlgorithmException { + return new RedirectView(provider.authenticate(session)); } @PostMapping("callback") - public RedirectView callback(@RequestParam("code") String code, @RequestParam("state") String state) throws IOException { + public RedirectView callback(@RequestParam("code") String code, HttpSession session) throws IOException { return new RedirectView(provider.callback(UniloginTokenCredentials.builder() .code(code) - .state(state) - .build() - ).getUrl(uniloginConf)); + .build(), session).getUrl(uniloginConf)); } @PostConstruct private void postConstruct() { - log.info("Exposing Unilogin OIDC Lightweight Endpoint..."); + log.info("Exposing Unilogin lightweight OIDC Endpoint..."); } }