This is a note on how to log in to Spring Boot using users managed by IAM Identity Center (formerly AWS SSO).
IAM Identity Center does not have OIDC Provider functionality, but it can be used as a SAML2 Identity Provider. Therefore, we will implement login using AWS users through SAML2 integration in Spring Boot.
SAML2 integration is provided by Spring Security.
https://docs.spring.io/spring-security/reference/servlet/saml2/index.html
Table of Contents
- Creating a Template Project
- Configuring the Identity Provider (Relying Party)
- Single Logout
- Authorization by Attributes
Creating a Template Project
First, create a template project using Spring Initializr.
curl -s https://start.spring.io/starter.tgz \
-d artifactId=hello-saml \
-d baseDir=hello-saml \
-d packageName=com.example \
-d dependencies=web,actuator,security,configuration-processor \
-d type=maven-project \
-d name=hello-saml \
-d applicationName=HelloSamlApplication | tar -xzvf -
cd hello-saml
To use SAML2 integration, add the following dependency to pom.xml.
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-saml2-service-provider</artifactId>
</dependency>
The library OpenSAML used by spring-security-saml2-service-provider is not available in Maven Central due to various reasons, so we will add the following Maven repository.
<project>
<!-- ... -->
<repositories>
<repository>
<id>shibboleth</id>
<url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>
</repository>
</repositories>
<!-- ... -->
</project>
Next, we will write the configuration for logging in with SAML2 in SecurityConfig.
cat <<'EOF' > src/main/java/com/example/SecurityConfig.java
package com.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/error").permitAll()
.anyRequest().authenticated())
.saml2Login(withDefaults())
.saml2Metadata(withDefaults())
.build();
}
}
EOF
Create a Controller to display user information authenticated via SAML2.
cat <<'EOF' > src/main/java/com/example/HelloController.java
package com.example;
import java.util.Map;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping(path = "/")
public Map<String, Object> hello(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal) {
return Map.of("username", principal.getName(), "attributes", principal.getAttributes());
}
}
EOF
Configuring the Identity Provider (Relying Party)
Next, we will configure the Identity Provider (Relying Party) in IAM Identity Center.
First, register the application by clicking the "Add application" button on the "Applications" screen.
Select "I have an application I want to set up".
Choose "Application type" as "SAML 2.0".
Set the "Display name" to "Hello SAML".
At this point, copy the URL of "IAM Identity Center SAML metadata file" under "IAM Identity Center metadata".
Set this URL in application.properties as follows:
METADATA_URL=https://portal.sso.****.amazonaws.com/saml/metadata/****
cat <<EOF > src/main/resources/application.properties
spring.application.name=hello-saml
spring.security.saml2.relyingparty.registration.awssso.entity-id=hello-saml
spring.security.saml2.relyingparty.registration.awssso.assertingparty.metadata-uri=$METADATA_URL
EOF
Once the application.properties is configured, start the application.
./mvnw spring-boot:run
The metadata for the Service Provider will be published at /saml2/metadata/<registrationId>, so download it.
curl -s http://localhost:8080/saml2/metadata/awssso -o ~/Downloads/metadata-awssso.xml
Upload the downloaded metadata in "Application metadata" and click "Submit".
Once the application is created and the "Status" is "Active", you are good to go.
Next, assign users and groups to this application by clicking the "Assign users and groups" button under "Assigned users and groups".
If there are no users or groups, create them. Here, I assigned the developers group and the administrators group.
Next, perform the attribute mapping. Click "Actions" in the application's "Details" and select "Edit attribute mappings".
Refer to https://docs.aws.amazon.com/singlesignon/latest/userguide/attributemappingsconcept.html for mapping.
Here, I mapped as follows:
Subject(used as username) ...${user.email}(typeemailAddress)firstName...${user.givenName}(typeunspecified)lastName...${user.familyName}(typeunspecified)groups...${user.groups}(typeunspecified)
After clicking "Save changes" to save the settings, access the application you started earlier (http://localhost:8080).
You will be redirected to the IAM Identity Center login screen.
After logging into IAM Identity Center, you will be redirected again.
You will be redirected to http://localhost:8080, and the logged-in user information will be displayed.
The information about the groups the user belongs to is included in the groups attribute, but you will receive the ID instead of the name.
You can check the Group ID in the details screen of the IAM Identity Center group.
Now you can log in to the Spring Boot application using IAM Identity Center users with SAML2.
Single Logout
Next, we will configure single logout.
Add the following configuration to SecurityConfig.
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/error").permitAll()
.anyRequest().authenticated())
.saml2Login(withDefaults())
.saml2Logout(withDefaults()) // <---
.saml2Metadata(withDefaults())
.build();
}
}
Single logout requests require signing. Set the private and public keys with the following command.
openssl req -x509 -newkey rsa:4096 -keyout src/main/resources/key.pem -out src/main/resources/cert.pem -sha256 -days 3650 -nodes -subj "/CN=@making/O=LOL.MAKI/C=JP"
Append the following settings to application.properties.
cat <<'EOF' >> src/main/resources/application.properties
spring.security.saml2.relyingparty.registration.awssso.signing.credentials[0].certificate-location=classpath:cert.pem
spring.security.saml2.relyingparty.registration.awssso.signing.credentials[0].private-key-location.=classpath:key.pem
spring.security.saml2.relyingparty.registration.awssso.singlelogout.binding=post
spring.security.saml2.relyingparty.registration.awssso.singlelogout.response-url={baseUrl}/logout/saml2/slo/{registrationId}
EOF
Stop and restart the application.
./mvnw spring-boot:run
After accessing http://localhost:8080 once, go to http://localhost:8080/logout.
Click the "Log Out" button.
This will log you out of the application, but you will not be logged out of IAM Identity Center.
You will be redirected to the portal screen after logging into IAM Identity Center.
Normally, this setting should allow logout from the Identity Provider, but it did not work with IAM Identity Center.
I found the following statement, so single logout may not be supported in IAM Identity Center.
https://github.com/aws-mwaa/upstream-to-airflow/blob/0a816c6f0b500e1b0515452e38e3446412f3e8e3/airflow/providers/amazon/aws/auth_manager/views/auth.py#L105
In fact, single logout worked with the same settings for Microsoft Entra ID (formerly Azure AD).
This may be a limitation of IAM Identity Center, so I will leave it as is.
Authorization by Attributes
Users logged in via SAML2 have the ROLE_USER authority by default.
The documentation describes how to customize the created user information.
Using this method, you can add any authority to the user.
However, it is more convenient to directly set authorization using the attributes held by the user information, so we will implement the following AuthorizationManager.
cat <<'EOF' > src/main/java/com/example/SamlAuthorizationManager.java
package com.example;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
public class SamlAuthorizationManager<T> implements AuthorizationManager<RequestAuthorizationContext> {
private final Function<Saml2AuthenticatedPrincipal, T> applyer;
private final Predicate<T> predicate;
public SamlAuthorizationManager(Function<Saml2AuthenticatedPrincipal, T> applyer, Predicate<T> predicate) {
this.applyer = applyer;
this.predicate = predicate;
}
public static SamlAuthorizationManager<List<String>> attribute(String name, Predicate<List<String>> predicate) {
return new SamlAuthorizationManager<>(principal -> principal.getAttribute(name), predicate);
}
public static SamlAuthorizationManager<String> firstAttribute(String name, Predicate<String> predicate) {
return new SamlAuthorizationManager<>(principal -> principal.getFirstAttribute(name), predicate);
}
public static SamlAuthorizationManager<String> username(Predicate<String> predicate) {
return new SamlAuthorizationManager<>(AuthenticatedPrincipal::getName, predicate);
}
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
Authentication auth = authentication.get();
if (auth instanceof Saml2Authentication && auth.getPrincipal() instanceof Saml2AuthenticatedPrincipal principal) {
T target = this.applyer.apply(principal);
if (target == null) {
return new AuthorizationDecision(false);
}
return new AuthorizationDecision(this.predicate.test(target));
}
else {
return new AuthorizationDecision(false);
}
}
}
EOF
For example, to access the /admin path, the user must meet one of the following conditions:
- Include
a7443a38-70d1-709f-aa2c-4841adf65ed1in thegroupsattribute - The
emailattribute (only the first element) ends with@example.com - The username is
adminormakingx@gmail.com
These conditions can be defined using SamlAuthorizationManager as follows.
// ...
import static com.example.SamlAuthorizationManager.attribute;
import static com.example.SamlAuthorizationManager.firstAttribute;
import static com.example.SamlAuthorizationManager.username;
import static org.springframework.security.authorization.AuthorizationManagers.anyOf;
// ...
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/error").permitAll()
// <---
.requestMatchers("/admin").access(anyOf(
attribute("groups", groups -> groups.contains("a7443a38-70d1-709f-aa2c-4841adf65ed1")),
firstAttribute("email", email -> email.endsWith("@example.com")),
username(username -> username.equals("admin") || username.equals("makingx@gmail.com"))))
// --->
.anyRequest().authenticated())
.saml2Login(withDefaults())
.saml2Metadata(withDefaults())
.saml2Logout(withDefaults())
.build();
}
Create a Controller for the /admin path.
cat <<'EOF' > src/main/java/com/example/AdminController.java
package com.example;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AdminController {
@GetMapping(path = "/admin")
public String admin() {
return "admin page";
}
}
EOF
Restart the application and access http://localhost:8080/admin.
If you log in with an authorized user, the page will be displayed as follows.
If you log in with an unauthorized user, a 403 error will be displayed.
By integrating IAM Identity Center with SAML2 in Spring Boot, we were able to log in to the application using AWS users.
Although single logout did not work, it is a convenient method if you are already managing users in IAM Identity Center and do not want to duplicate user management.