diff --git a/endpoints-api-modified.yaml b/endpoints-api-modified.yaml index 152a8d8..d932bbd 100644 --- a/endpoints-api-modified.yaml +++ b/endpoints-api-modified.yaml @@ -183,24 +183,24 @@ paths: application/trust-mark+jwt: schema: type: string - 400: - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - 404: - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - 500: - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' +# 400: +# description: Bad Request +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/ErrorResponse' +# 404: +# description: Not Found +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/ErrorResponse' +# 500: +# description: Server Error +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/ErrorResponse' /{name}/trust_mark_listing: get: @@ -233,24 +233,24 @@ paths: type: array items: type: string - 400: - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - 404: - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - 500: - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' +# 400: +# description: Bad Request +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/ErrorResponse' +# 404: +# description: Not Found +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/ErrorResponse' +# 500: +# description: Server Error +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/ErrorResponse' /{name}/trust_mark_status: post: diff --git a/pom.xml b/pom.xml index e36cbca..b528a50 100644 --- a/pom.xml +++ b/pom.xml @@ -193,173 +193,175 @@ - - - - org.springframework.boot - spring-boot-maven-plugin - - eudiw-wallet-issuer-poc - - true - - - - org.projectlombok - lombok - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - ${java.version} - - - org.projectlombok - lombok - ${lombok.version} - - - - - - - - org.openapitools - openapi-generator-maven-plugin - ${openapi-generator.version} - - - - generate - - - ${project.basedir}/endpoints-api-modified.yaml - java - - resttemplate - false - true - false - - se.digg.eudiw - se.digg.eudiw.client - se.digg.eudiw.model - ${project.build.directory}/generated-sources/openapi - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - - org.apache.maven.plugins - maven-failsafe-plugin - ${maven-failsafe-plugin.version} - - - - - org.apache.maven.plugins - maven-javadoc-plugin - ${maven-javadoc-plugin.version} - - - attach-javadocs - - jar - - - - - - - - org.apache.maven.plugins - maven-source-plugin - ${maven-source-plugin.version} - - - attach-sources - - jar - - - - - - - - org.apache.maven.plugins - maven-deploy-plugin - ${maven-deploy-plugin.version} - - local::file:./target/staging-deploy - false - - - - - - org.jreleaser - jreleaser-maven-plugin - ${jreleaser-maven-plugin.version} - - ${project.basedir}/jreleaser.yml - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - ${maven-enforcer-plugin.version} - - - enforce-versions - - enforce - - - - - - [3.8.0,) - Maven 3.8.0 or higher is required - - - - - [21,) - Java 21 or higher is required - - - - - - - - - - - - - true - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + eudiw-wallet-issuer-poc + + true + + + + org.projectlombok + lombok + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + + org.openapitools + openapi-generator-maven-plugin + ${openapi-generator.version} + + + + generate + + + ${project.basedir}/endpoints-api-modified.yaml + java + + resttemplate + false + true + false + + false + false + se.digg.eudiw + se.digg.eudiw.client + se.digg.eudiw.model + ${project.build.directory}/generated-sources/openapi + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + local::file:./target/staging-deploy + false + + + + + + org.jreleaser + jreleaser-maven-plugin + ${jreleaser-maven-plugin.version} + + ${project.basedir}/jreleaser.yml + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + enforce-versions + + enforce + + + + + + [3.8.0,) + Maven 3.8.0 or higher is required + + + + + [21,) + Java 21 or higher is required + + + + + + + + + + + + + true + + + + + \ No newline at end of file diff --git a/src/main/java/se/digg/eudiw/authorization/EudiwAccessTokenCustomizer.java b/src/main/java/se/digg/eudiw/authorization/EudiwAccessTokenCustomizer.java new file mode 100644 index 0000000..a3da8b1 --- /dev/null +++ b/src/main/java/se/digg/eudiw/authorization/EudiwAccessTokenCustomizer.java @@ -0,0 +1,62 @@ +package se.digg.eudiw.authorization; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsSet; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; +import se.digg.eudiw.authentication.SwedenConnectPrincipal; + +import java.util.Set; +import java.util.stream.Collectors; + +public class EudiwAccessTokenCustomizer implements OAuth2TokenCustomizer { + public EudiwAccessTokenCustomizer() { + } + + @Override + public void customize(OAuth2TokenClaimsContext context) { + Authentication principal = context.getPrincipal(); + if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) { + Set authorities = principal.getAuthorities().stream().map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + + context.getClaims().claim("authorities", authorities); + + // TODO det mesta borde flyttas till idToken eller userinfo någonting + OAuth2TokenClaimsSet.Builder claims = context.getClaims(); + if (principal.getPrincipal() instanceof SwedenConnectPrincipal) { + SwedenConnectPrincipal p = (SwedenConnectPrincipal) principal.getPrincipal(); + if (p.getSubjAttributes().getPersonalNumber() != null) claims.claim("personalNumber", p.getSubjAttributes().getPersonalNumber()); + if (p.getSubjAttributes().getName() != null) claims.claim("name", p.getSubjAttributes().getName()); + if (p.getSubjAttributes().getGivenName() != null) claims.claim("givenName", p.getSubjAttributes().getGivenName()); + if (p.getSubjAttributes().getSurname() != null) claims.claim("surname", p.getSubjAttributes().getSurname()); + if (p.getSubjAttributes().getCoordinationNumber() != null) claims.claim("coordinationNumber", p.getSubjAttributes().getCoordinationNumber()); + if (p.getSubjAttributes().getPrid() != null) claims.claim("prid", p.getSubjAttributes().getPrid()); + if (p.getSubjAttributes().getPridPersistence() != null) claims.claim("pridPersistence", p.getSubjAttributes().getPridPersistence()); + if (p.getSubjAttributes().getEidasPersonIdentifier() != null) claims.claim("eidasPersonIdentifier", p.getSubjAttributes().getEidasPersonIdentifier()); + if (p.getSubjAttributes().getPersonalNumberBinding() != null) claims.claim("personalNumberBinding", p.getSubjAttributes().getPersonalNumberBinding()); + if (p.getSubjAttributes().getOrgNumber() != null) claims.claim("orgNumber", p.getSubjAttributes().getOrgNumber()); + if (p.getSubjAttributes().getOrgAffiliation() != null) claims.claim("orgAffiliation", p.getSubjAttributes().getOrgAffiliation()); + if (p.getSubjAttributes().getOrgName() != null) claims.claim("orgName", p.getSubjAttributes().getOrgName()); + if (p.getSubjAttributes().getOrgUnit() != null) claims.claim("orgUnit", p.getSubjAttributes().getOrgUnit()); + if (p.getSubjAttributes().getUserCertificate() != null) claims.claim("userCertificate", p.getSubjAttributes().getUserCertificate()); + if (p.getSubjAttributes().getUserSignature() != null) claims.claim("userSignature", p.getSubjAttributes().getUserSignature()); + if (p.getSubjAttributes().getDeviceIp() != null) claims.claim("deviceIp", p.getSubjAttributes().getDeviceIp()); + if (p.getSubjAttributes().getAuthnEvidence() != null) claims.claim("authnEvidence", p.getSubjAttributes().getAuthnEvidence()); + if (p.getSubjAttributes().getCountry() != null) claims.claim("country", p.getSubjAttributes().getCountry()); + if (p.getSubjAttributes().getBirthName() != null) claims.claim("birthName", p.getSubjAttributes().getBirthName()); + if (p.getSubjAttributes().getPlaceOfbirth() != null) claims.claim("placeOfbirth", p.getSubjAttributes().getPlaceOfbirth()); + if (p.getSubjAttributes().getAge() != null) claims.claim("age", p.getSubjAttributes().getAge()); + if (p.getSubjAttributes().getBirthDate() != null) claims.claim("birthDate", p.getSubjAttributes().getBirthDate()); + + claims.claim("client_id", context.getRegisteredClient().getClientId()); + + } + + } + } +} diff --git a/src/main/java/se/digg/eudiw/authorization/EudiwJwtGenerator.java b/src/main/java/se/digg/eudiw/authorization/EudiwJwtGenerator.java new file mode 100644 index 0000000..ca10444 --- /dev/null +++ b/src/main/java/se/digg/eudiw/authorization/EudiwJwtGenerator.java @@ -0,0 +1,151 @@ +package se.digg.eudiw.authorization; + +import org.springframework.lang.Nullable; +import org.springframework.security.core.session.SessionInformation; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.*; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; +import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.UUID; +import org.springframework.security.oauth2.jwt.JwsHeader; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.JwtEncoderParameters; +import se.digg.eudiw.authentication.SwedenConnectPrincipal; + +// TODO vi borde kunna konfigurera endast en jwtCustomizer men av någon anledning har det krånglat... + +public final class EudiwJwtGenerator implements OAuth2TokenGenerator { + private final JwtEncoder jwtEncoder; + private OAuth2TokenCustomizer jwtCustomizer; + + public EudiwJwtGenerator(JwtEncoder jwtEncoder) { + Assert.notNull(jwtEncoder, "jwtEncoder cannot be null"); + this.jwtEncoder = jwtEncoder; + } + + @Nullable + public Jwt generate(OAuth2TokenContext context) { + if (context.getTokenType() != null && (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) || "id_token".equals(context.getTokenType().getValue()))) { + if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) && !OAuth2TokenFormat.SELF_CONTAINED.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) { + return null; + } else { + String issuer = null; + if (context.getAuthorizationServerContext() != null) { + issuer = context.getAuthorizationServerContext().getIssuer(); + } + + RegisteredClient registeredClient = context.getRegisteredClient(); + Instant issuedAt = Instant.now(); + JwsAlgorithm jwsAlgorithm = SignatureAlgorithm.RS256; + Instant expiresAt; + if ("id_token".equals(context.getTokenType().getValue())) { + expiresAt = issuedAt.plus(30L, ChronoUnit.MINUTES); + if (registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm() != null) { + jwsAlgorithm = registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm(); + } + } else { + expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive()); + } + + JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder(); + if (StringUtils.hasText(issuer)) { + claimsBuilder.issuer(issuer); + } + + if (context.getPrincipal().getPrincipal() instanceof SwedenConnectPrincipal) { + SwedenConnectPrincipal swedenConnectPrincipal = (SwedenConnectPrincipal)context.getPrincipal().getPrincipal(); + claimsBuilder.claim("personalNumber", swedenConnectPrincipal.getSubjAttributes().getPersonalNumber()); + claimsBuilder.claim("surname", swedenConnectPrincipal.getSubjAttributes().getSurname()); + claimsBuilder.claim("givenName", swedenConnectPrincipal.getSubjAttributes().getGivenName()); + claimsBuilder.claim("birthDate", swedenConnectPrincipal.getSubjAttributes().getBirthDate()); + } + + claimsBuilder.claim("clientId", registeredClient.getClientId()); + + claimsBuilder.subject(context.getPrincipal().getName()).audience(Collections.singletonList(registeredClient.getClientId())).issuedAt(issuedAt).expiresAt(expiresAt).id(UUID.randomUUID().toString()); + if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) { + claimsBuilder.notBefore(issuedAt); + if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) { + claimsBuilder.claim("scope", context.getAuthorizedScopes()); + } + } else if ("id_token".equals(context.getTokenType().getValue())) { + claimsBuilder.claim("azp", registeredClient.getClientId()); + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getAuthorizationGrantType())) { + OAuth2AuthorizationRequest authorizationRequest = (OAuth2AuthorizationRequest)context.getAuthorization().getAttribute(OAuth2AuthorizationRequest.class.getName()); + String nonce = (String)authorizationRequest.getAdditionalParameters().get("nonce"); + if (StringUtils.hasText(nonce)) { + claimsBuilder.claim("nonce", nonce); + } + + SessionInformation sessionInformation = (SessionInformation)context.get(SessionInformation.class); + if (sessionInformation != null) { + claimsBuilder.claim("sid", sessionInformation.getSessionId()); + claimsBuilder.claim("auth_time", sessionInformation.getLastRequest()); + } + } else if (AuthorizationGrantType.REFRESH_TOKEN.equals(context.getAuthorizationGrantType())) { + OidcIdToken currentIdToken = (OidcIdToken)context.getAuthorization().getToken(OidcIdToken.class).getToken(); + if (currentIdToken.hasClaim("sid")) { + claimsBuilder.claim("sid", currentIdToken.getClaim("sid")); + } + + if (currentIdToken.hasClaim("auth_time")) { + claimsBuilder.claim("auth_time", currentIdToken.getClaim("auth_time")); + } + } + } + + JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(jwsAlgorithm); + if (this.jwtCustomizer != null) { + JwtEncodingContext.Builder jwtContextBuilder = (JwtEncodingContext.Builder)((JwtEncodingContext.Builder)((JwtEncodingContext.Builder)((JwtEncodingContext.Builder)((JwtEncodingContext.Builder)((JwtEncodingContext.Builder)JwtEncodingContext.with(jwsHeaderBuilder, claimsBuilder).registeredClient(context.getRegisteredClient())).principal(context.getPrincipal())).authorizationServerContext(context.getAuthorizationServerContext())).authorizedScopes(context.getAuthorizedScopes())).tokenType(context.getTokenType())).authorizationGrantType(context.getAuthorizationGrantType()); + if (context.getAuthorization() != null) { + jwtContextBuilder.authorization(context.getAuthorization()); + } + + if (context.getAuthorizationGrant() != null) { + jwtContextBuilder.authorizationGrant(context.getAuthorizationGrant()); + } + + if ("id_token".equals(context.getTokenType().getValue())) { + SessionInformation sessionInformation = (SessionInformation)context.get(SessionInformation.class); + if (sessionInformation != null) { + jwtContextBuilder.put(SessionInformation.class, sessionInformation); + } + } + + JwtEncodingContext jwtContext = jwtContextBuilder.build(); + this.jwtCustomizer.customize(jwtContext); + } + + JwsHeader jwsHeader = jwsHeaderBuilder.build(); + JwtClaimsSet claims = claimsBuilder.build(); + Jwt jwt = this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims)); + return jwt; + } + } else { + return null; + } + } + + public void setJwtCustomizer(OAuth2TokenCustomizer jwtCustomizer) { + Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null"); + this.jwtCustomizer = jwtCustomizer; + } +} diff --git a/src/main/java/se/digg/eudiw/authorization/FederatedRegisteredClientRepository.java b/src/main/java/se/digg/eudiw/authorization/FederatedRegisteredClientRepository.java deleted file mode 100644 index 91c0595..0000000 --- a/src/main/java/se/digg/eudiw/authorization/FederatedRegisteredClientRepository.java +++ /dev/null @@ -1,38 +0,0 @@ -package se.digg.eudiw.authorization; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; -import org.springframework.stereotype.Component; -import se.digg.eudiw.config.EudiwConfig; -import se.digg.eudiw.service.OpenIdFederationService; -import se.digg.wallet.metadata.WalletOAuthClientMetadata; - -@Component -public class FederatedRegisteredClientRepository implements RegisteredClientRepository { - - private final String walletProviderAnchor; - private final OpenIdFederationService openIdFederationService; - - - FederatedRegisteredClientRepository(@Autowired EudiwConfig eudiwConfig, @Autowired OpenIdFederationService openIdFederationService) { - this.walletProviderAnchor = eudiwConfig.getOpenidFederation().walletProviderAnchor(); - this.openIdFederationService = openIdFederationService; - } - - @Override - public void save(RegisteredClient registeredClient) { - - } - - @Override - public RegisteredClient findById(String id) { - return null; - } - - @Override - public RegisteredClient findByClientId(String clientId) { - WalletOAuthClientMetadata walletOAuthClientMetadata = openIdFederationService.resolveWallet(clientId); - return null; - } -} diff --git a/src/main/java/se/digg/eudiw/authorization/OidFederatedRegisteredClientRepository.java b/src/main/java/se/digg/eudiw/authorization/OidFederatedRegisteredClientRepository.java new file mode 100644 index 0000000..ef0e4ed --- /dev/null +++ b/src/main/java/se/digg/eudiw/authorization/OidFederatedRegisteredClientRepository.java @@ -0,0 +1,122 @@ +package se.digg.eudiw.authorization; + +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; +import org.springframework.util.Assert; +import se.digg.eudiw.config.EudiwConfig; +import se.digg.eudiw.service.OpenIdFederationService; +import se.digg.wallet.metadata.WalletOAuthClientMetadata; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class OidFederatedRegisteredClientRepository implements RegisteredClientRepository { + + private final EudiwConfig config; + private final OpenIdFederationService openIdFederationService; + private final Map idRegistrationMap; + private final Map clientIdRegistrationMap; + + public OidFederatedRegisteredClientRepository(EudiwConfig config, OpenIdFederationService openIdFederationService ) { + this.config = config; + this.openIdFederationService = openIdFederationService; + ConcurrentHashMap idRegistrationMapResult = new ConcurrentHashMap<>(); + ConcurrentHashMap clientIdRegistrationMapResult = new ConcurrentHashMap<>(); + this.idRegistrationMap = idRegistrationMapResult; + this.clientIdRegistrationMap = clientIdRegistrationMapResult; + } + + @Override + public void save(RegisteredClient registeredClient) { + // no need to save - the client registration is done in federation + } + + @Override + public RegisteredClient findById(String id) { + Assert.hasText(id, "id cannot be empty"); + return (RegisteredClient)this.idRegistrationMap.get(id); + } + + @Override + public RegisteredClient findByClientId(String clientId) { + Assert.hasText(clientId, "clientId cannot be empty"); + RegisteredClient client = (RegisteredClient)this.clientIdRegistrationMap.get(clientId); + if (client == null) { + return buildRegisteredClient(clientId); + } + return client; + } + + + private RegisteredClient buildRegisteredClient(String clientId) { + + WalletOAuthClientMetadata metadata = openIdFederationService.resolveWallet(clientId); + + MessageDigest digest = null; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + + byte[] shasum = digest.digest(clientId.getBytes(StandardCharsets.UTF_8)); + String id = new String(shasum, StandardCharsets.UTF_8); + + RegisteredClient.Builder registeredClientBuilder = RegisteredClient.withId(id) + .clientId(clientId) + .clientAuthenticationMethods(s -> { + s.add(ClientAuthenticationMethod.NONE); + }) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .authorizationGrantType(new AuthorizationGrantType("urn:ietf:params:oauth:grant-type:pre-authorized_code")); + + + registeredClientBuilder = registeredClientBuilder.redirectUri(String.format("%s/login/oauth2/code/messaging-client-pkce", config.getIssuerBaseUrl())) + .redirectUri(String.format("%s/callback", config.getIssuerBaseUrl())) + .redirectUri(String.format("%s/auth-flow", config.getIssuerBaseUrl())) + .redirectUri(String.format("%s/callback-demo-auth", config.getIssuerBaseUrl())) + .redirectUri(String.format("%s/credentials", config.getIssuerBaseUrl())) + .redirectUri(String.format("%s/callback-demo-pre-auth", config.getIssuerBaseUrl())) + .redirectUri("com.example.eudiwdemo:/oauthredirect") + .redirectUri(config.getIssuerBaseUrl()); + + for (String uri : config.getRedirectUris()) { + registeredClientBuilder = registeredClientBuilder.redirectUri(uri); + } + + registeredClientBuilder = registeredClientBuilder.scope("identitycredential.read") + .scope("VerifiablePortableDocumentA1") + .scope(OidcScopes.OPENID) + .scope(OidcScopes.PROFILE) + .clientSettings(ClientSettings.builder() + .requireAuthorizationConsent(true) + .requireProofKey(true) //Only PKCE is supported + .build()) + .tokenSettings(TokenSettings.builder() + .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) // Generate JWT token + .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256) + .accessTokenTimeToLive(Duration.ofSeconds(30 * 60)) + .refreshTokenTimeToLive(Duration.ofSeconds(60 * 60)) + .reuseRefreshTokens(true) + .build()) + .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()); + + + RegisteredClient client = registeredClientBuilder.build(); + + + return client; + } +} diff --git a/src/main/java/se/digg/eudiw/authorization/PreAuthCodeGrantAuthenticationProvider.java b/src/main/java/se/digg/eudiw/authorization/PreAuthCodeGrantAuthenticationProvider.java index c9225c6..9eb3cec 100644 --- a/src/main/java/se/digg/eudiw/authorization/PreAuthCodeGrantAuthenticationProvider.java +++ b/src/main/java/se/digg/eudiw/authorization/PreAuthCodeGrantAuthenticationProvider.java @@ -11,8 +11,6 @@ import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; @@ -20,7 +18,6 @@ import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; -import se.digg.eudiw.context.EudiwSessionSecurityContextRepository; public class PreAuthCodeGrantAuthenticationProvider implements AuthenticationProvider { diff --git a/src/main/java/se/digg/eudiw/config/OAuth2ServerConfig.java b/src/main/java/se/digg/eudiw/config/OAuth2ServerConfig.java index 2f2b861..1525657 100644 --- a/src/main/java/se/digg/eudiw/config/OAuth2ServerConfig.java +++ b/src/main/java/se/digg/eudiw/config/OAuth2ServerConfig.java @@ -1,53 +1,30 @@ package se.digg.eudiw.config; -import java.io.IOException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; -import java.time.Duration; -import java.util.List; import java.util.UUID; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.proc.SecurityContext; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.annotation.Primary; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationEventPublisher; -import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.session.SessionRegistry; -import org.springframework.security.core.session.SessionRegistryImpl; -import org.springframework.security.oauth2.core.*; import org.springframework.security.oauth2.jwt.*; import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; -import org.springframework.security.oauth2.server.authorization.authentication.*; import org.springframework.security.oauth2.server.authorization.token.*; -import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter; -import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.*; -import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; -import org.springframework.security.web.session.HttpSessionEventPublisher; -import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; import se.digg.eudiw.authentication.*; -import se.digg.eudiw.authorization.OAuth2ParAuthorizationCodeRequestAuthenticationConverter; -import se.digg.eudiw.authorization.PreAuthCodeGrantAuthenticationConverter; -import se.digg.eudiw.authorization.PreAuthCodeGrantAuthenticationProvider; +import se.digg.eudiw.authorization.*; import se.digg.eudiw.context.EudiwSessionSecurityContextRepository; import org.springframework.beans.factory.annotation.Autowired; @@ -61,36 +38,28 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.core.oidc.OidcScopes; -import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; -import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; -import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; -import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; -import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; -import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; import org.springframework.security.web.SecurityFilterChain; import com.nimbusds.jose.jwk.source.JWKSource; +import se.digg.eudiw.service.OpenIdFederationService; import se.digg.eudiw.service.ParCacheService; @Configuration @EnableWebSecurity public class OAuth2ServerConfig { + + private static final Logger logger = LoggerFactory.getLogger(OAuth2ServerConfig.class); + @Autowired private SwedenConnectAuthenticationProvider authProvider; @Autowired EudiwSessionSecurityContextRepository contextRepository; - //@Autowired - //private ClientRegistrationRepository clientRegistrationRepository; @Autowired private EudiwConfig config; @@ -126,6 +95,7 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h .with(authorizationServerConfigurer, (authorizationServer) -> authorizationServer .registeredClientRepository(registeredClientRepository) + .tokenGenerator(tokenGenerator()) .oidc(Customizer.withDefaults()) // Enable OpenID Connect 1.0 .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint @@ -133,7 +103,6 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h .authenticationProvider(authProvider) //.authorizationRequestConverters(converters -> converters.addFirst(new ParAuthenticationConverter(parCacheService))) .errorResponseHandler((req, res, error) -> { - System.out.println("HJKSHKHDJHSAKDHKSHDJKHAJDHAKSHAKHDKAHSDJ******"); res.getWriter().write("FOOBAR!"); res.setStatus(HttpStatus.OK.value()); }) @@ -223,10 +192,10 @@ public IdProxyRequestBuilder idProxyRequestBuilder() { } @Bean - public RegisteredClientRepository registeredClientRepository() { - - + public RegisteredClientRepository registeredClientRepository(OpenIdFederationService openIdFederationService) { + return new OidFederatedRegisteredClientRepository(config, openIdFederationService); +/* RegisteredClient.Builder registeredClientBuilder = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId(config.getClientId()) .clientAuthenticationMethods(s -> { @@ -268,6 +237,8 @@ public RegisteredClientRepository registeredClientRepository() { .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()); RegisteredClient registeredClient = registeredClientBuilder.build(); return new InMemoryRegisteredClientRepository(registeredClient); + + */ } public JwtDecoder jwtDecoder(JWKSource jwkSource) { @@ -291,49 +262,6 @@ public JWKSource jwkSource() { return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); } - @Bean - public OAuth2TokenCustomizer accessTokenCustomizer() { - return (context) -> { - Authentication principal = context.getPrincipal(); - if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) { - Set authorities = principal.getAuthorities().stream().map(GrantedAuthority::getAuthority) - .collect(Collectors.toSet()); - - context.getClaims().claim("authorities", authorities); - - // TODO det mesta borde flyttas till idToken eller userinfo någonting - JwtClaimsSet.Builder claims = context.getClaims(); - if (principal.getPrincipal() instanceof SwedenConnectPrincipal) { - SwedenConnectPrincipal p = (SwedenConnectPrincipal) principal.getPrincipal(); - if (p.getSubjAttributes().getPersonalNumber() != null) claims.claim("personalNumber", p.getSubjAttributes().getPersonalNumber()); - if (p.getSubjAttributes().getName() != null) claims.claim("name", p.getSubjAttributes().getName()); - if (p.getSubjAttributes().getGivenName() != null) claims.claim("givenName", p.getSubjAttributes().getGivenName()); - if (p.getSubjAttributes().getSurname() != null) claims.claim("surname", p.getSubjAttributes().getSurname()); - if (p.getSubjAttributes().getCoordinationNumber() != null) claims.claim("coordinationNumber", p.getSubjAttributes().getCoordinationNumber()); - if (p.getSubjAttributes().getPrid() != null) claims.claim("prid", p.getSubjAttributes().getPrid()); - if (p.getSubjAttributes().getPridPersistence() != null) claims.claim("pridPersistence", p.getSubjAttributes().getPridPersistence()); - if (p.getSubjAttributes().getEidasPersonIdentifier() != null) claims.claim("eidasPersonIdentifier", p.getSubjAttributes().getEidasPersonIdentifier()); - if (p.getSubjAttributes().getPersonalNumberBinding() != null) claims.claim("personalNumberBinding", p.getSubjAttributes().getPersonalNumberBinding()); - if (p.getSubjAttributes().getOrgNumber() != null) claims.claim("orgNumber", p.getSubjAttributes().getOrgNumber()); - if (p.getSubjAttributes().getOrgAffiliation() != null) claims.claim("orgAffiliation", p.getSubjAttributes().getOrgAffiliation()); - if (p.getSubjAttributes().getOrgName() != null) claims.claim("orgName", p.getSubjAttributes().getOrgName()); - if (p.getSubjAttributes().getOrgUnit() != null) claims.claim("orgUnit", p.getSubjAttributes().getOrgUnit()); - if (p.getSubjAttributes().getUserCertificate() != null) claims.claim("userCertificate", p.getSubjAttributes().getUserCertificate()); - if (p.getSubjAttributes().getUserSignature() != null) claims.claim("userSignature", p.getSubjAttributes().getUserSignature()); - if (p.getSubjAttributes().getDeviceIp() != null) claims.claim("deviceIp", p.getSubjAttributes().getDeviceIp()); - if (p.getSubjAttributes().getAuthnEvidence() != null) claims.claim("authnEvidence", p.getSubjAttributes().getAuthnEvidence()); - if (p.getSubjAttributes().getCountry() != null) claims.claim("country", p.getSubjAttributes().getCountry()); - if (p.getSubjAttributes().getBirthName() != null) claims.claim("birthName", p.getSubjAttributes().getBirthName()); - if (p.getSubjAttributes().getPlaceOfbirth() != null) claims.claim("placeOfbirth", p.getSubjAttributes().getPlaceOfbirth()); - if (p.getSubjAttributes().getAge() != null) claims.claim("age", p.getSubjAttributes().getAge()); - if (p.getSubjAttributes().getBirthDate() != null) claims.claim("birthDate", p.getSubjAttributes().getBirthDate()); - } - - } - }; - } - - static class Jwks { private Jwks() { @@ -371,7 +299,7 @@ static KeyPair generateRsaKey() { @Bean public OAuth2TokenGenerator tokenGenerator() { JwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource()); - JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder); + EudiwJwtGenerator jwtGenerator = new EudiwJwtGenerator(jwtEncoder); OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator(); OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); return new DelegatingOAuth2TokenGenerator( diff --git a/src/main/java/se/digg/eudiw/controllers/CredentialController.java b/src/main/java/se/digg/eudiw/controllers/CredentialController.java index b219dcf..6fed76f 100644 --- a/src/main/java/se/digg/eudiw/controllers/CredentialController.java +++ b/src/main/java/se/digg/eudiw/controllers/CredentialController.java @@ -5,6 +5,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.nimbusds.jose.jwk.JWK; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -28,6 +31,9 @@ @RestController public class CredentialController { + + private static final Logger logger = LoggerFactory.getLogger(CredentialController.class); + private final EudiwConfig eudiwConfig; private final SignerConfig signerConfig; private final OpenIdFederationService openIdFederationService; @@ -68,7 +74,8 @@ String credential(@AuthenticationPrincipal Jwt jwt, @RequestBody CredentialParam Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication.getPrincipal() instanceof Jwt) { - WalletOAuthClientMetadata walletOAuthClientMetadata = openIdFederationService.resolveWallet("1234567891"); + String clientId = jwt.getClaim("clientId"); + WalletOAuthClientMetadata walletOAuthClientMetadata = openIdFederationService.resolveWallet(clientId); Optional jwk; if (walletOAuthClientMetadata != null) { jwk = walletOAuthClientMetadata.getJwkSet().getKeys().stream().findFirst(); @@ -80,11 +87,17 @@ String credential(@AuthenticationPrincipal Jwt jwt, @RequestBody CredentialParam // TODO - get PID data from ID token and authentic source (t.ex. skatteverket) PidBuilder builder = new PidBuilder(eudiwConfig.getIssuer(), signerConfig) .withExp(eudiwConfig.getExpHours()) - .withVcType("https://attestations.eudiw.se/se_pid") - .addSelectiveDisclosure("given_name", "FOO" + jwt.getClaim("givenName")) - .addSelectiveDisclosure("last_name", "FOO" + jwt.getClaim("surname")) - .addSelectiveDisclosure("birthdate", "FOO" + jwt.getClaim("birthDate")) - .addSelectiveDisclosure("address", new Address("123 Main St", "Anytown", "Anystate", "US")); + .withVcType("https://attestations.eudiw.se/se_pid"); + + + if (!StringUtils.isBlank(jwt.getClaim("givenName"))) + builder.addSelectiveDisclosure("given_name", jwt.getClaim("givenName")); + if (!StringUtils.isBlank(jwt.getClaim("surname"))) + builder.addSelectiveDisclosure("last_name", jwt.getClaim("surname")); + if (!StringUtils.isBlank(jwt.getClaim("birthDate"))) + builder.addSelectiveDisclosure("birthdate", jwt.getClaim("birthDate")); + + builder.addSelectiveDisclosure("address", new Address("123 Main St", "Anytown", "Anystate", "US")); jwk.ifPresent(value -> builder.withCnf(Map.of("jwk", value.toPublicJWK().toJSONObject()))); diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index ddce88d..0163d1e 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -42,7 +42,8 @@ eudiw: authHost: https://local.dev.swedenconnect.se:9090 credentialHost: https://local.dev.swedenconnect.se:9090 expHours: 24 - clientId: eudiw-client + clientId: 1234567891 + #eudiw-client openidFederation: baseUrl: https://local.dev.swedenconnect.se:9040/oidfed trustMarkId: https://local.dev.swedenconnect.se/trust-mark-id/pid-issuer