Spring boot and Okta SAML2 - okta

I like the idea of using spring-security-saml2-service-provider - from of docs: https://docs.spring.io/spring-security/reference/5.6.0-RC1/servlet/saml2/index.html
Instead of spring-security-saml2-core it looks way less boilerplate, but I catch 400 response when I send App Embed Link from Okta admin app. Through debug it seems that
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
AbstractSaml2AuthenticationRequest authenticationRequest = this.authenticationRequestResolver.resolve(request);
if (authenticationRequest == null) {
filterChain.doFilter(request, response);..}
can't resolve the incoming request,but I am not sure whether it's related.
My yml config:
security:
saml2:
relyingparty:
registration:
okta:
identityprovider:
entity-id: http://www.okta.com/exk1juy5xrR5BsW44697
verification.credentials:
- certificate-location: "classpath:saml/okta.cert"
singlesignon.url: https://trial-8410773.okta.com/app/trial-8410773_templatemanager_2/exk1juy5xrR5BsW44697/sso/saml
singlesignon.sign-request: false
assertingparty.metadata-uri: https://trial-8410773.okta.com/app/trial-8410773_templatemanager_2/exk1juy5xrR5BsW44697/sso/saml/metadata
My Okta config:
GENERAL
Single Sign On URLhttp://localhost:8080/api/v1/saml2/SSO
Requestable SSO URLsURLIndex
http://localhost:8080/api/v1/saml2/SSO0Recipient URLhttp://localhost:8080/api/v1/saml2/SSODestination URLhttp://localhost:8080/api/v1/saml2/SSOAudience Restrictionhttp://localhost:8080/saml/metadata
Also I provide endpoint for saml authentication:
#RequestMapping(SsoAuthenticationController.BASE_NAME)
public interface SsoAuthenticationController {
final String BASE_NAME = "/v1/saml2/SSO";
#GetMapping("/")
public ResponseEntity<HttpStatus> index( Saml2AuthenticatedPrincipal principal) ;
}
Actual security config:
http.cors()
.and()
.csrf()
.disable()
.authorizeRequests()
.antMatchers(SECURITY_WHITELIST)
.permitAll()
.anyRequest()
.authenticated()
/*.and()
.httpBasic()
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint).and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)*/
.and()
.saml2Login(Customizer.withDefaults());
Here is Saml interceptor's logs for google chrome:
https://pastebin.com/Be3NZe5B
Any ideas?

I created a Spring Boot 3 + SAML example with Okta recently. Hopefully, these instructions help.
Create a Spring Boot app using start.spring.io. Select the following options:
Project: Gradle
Spring Boot: 3.0.0 (SNAPSHOT)
Dependencies: Spring Web, Spring Security, Thymeleaf
Add src/main/java/com/example/demo/HomeController.java to populate the authenticated user's information.
package com.example.demo;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
#Controller
public class HomeController {
#RequestMapping("/")
public String home(#AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
model.addAttribute("name", principal.getName());
model.addAttribute("emailAddress", principal.getFirstAttribute("email"));
model.addAttribute("userAttributes", principal.getAttributes());
return "home";
}
}
Create a src/main/resources/templates/home.html file to render the user's information.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<head>
<title>Spring Boot and SAML</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<h1>Welcome</h1>
<p>You are successfully logged in as <span sec:authentication="name"></span></p>
<p>Your email address is <span th:text="${emailAddress}"></span>.</p>
<p>Your authorities are <span sec:authentication="authorities"></span>.</p>
<h2>All Your Attributes</h2>
<dl th:each="userAttribute : ${userAttributes}">
<dt th:text="${userAttribute.key}"></dt>
<dd th:text="${userAttribute.value}"></dd>
</dl>
<form th:action="#{/logout}" method="post">
<button id="logout" type="submit">Logout</button>
</form>
</body>
</html>
Create a src/main/resources/application.yml file to contain your metadata URI.
spring:
security:
saml2:
relyingparty:
registration:
assertingparty:
metadata-uri: <your-metadata-uri>
Then, change build.gradle to use thymeleaf-extras-springsecurity6 instead of thymeleaf-extras-springsecurity5 and add Spring Security SAML's dependency:
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
implementation 'org.springframework.security:spring-security-saml2-service-provider'
To get the metadata URI from Okta, log in to your account and go to Applications > Create App Integration. Select SAML 2.0 and click Next. Name your app something like Spring Boot SAML and click Next.
Use the following settings:
Single sign on URL: http://localhost:8080/login/saml2/sso/okta
Use this for Recipient URL and Destination URL: ✅ (the default)
Audience URI: http://localhost:8080/saml2/service-provider-metadata/okta
Then click Next. Select the following options:
I'm an Okta customer adding an internal app
This is an internal app that we have created
Select Finish.
Okta will create your app, and you will be redirected to its Sign On tab. Scroll down to the SAML Signing Certificates and go to SHA-2 > Actions > View IdP Metadata. You can right-click and copy this menu item's link or open its URL. Copy the resulting link to your clipboard. It should look something like the following:
https://dev-13337.okta.com/app/<random-characters>/sso/saml/metadata
Go to your app's Assignment tab and assign access to the Everyone group.
Paste your metadata URI in to your application.yml file. Start the app using ./gradlew bootRun and open http://localhost:8080 in your favorite browser. You should be redirected to login.

Related

How to avoid login page with Spring Ssecurity and SAML?

I am trying that when a user logs into my system via Saml2, it automatically redirects him to the associated configuration based on his domain, without having to go through the login page like the one shown.
For example the user: user1#company1.com, I would like to be automatically redirected to the authentication page corresponding to the domain (company1 > singlesignon.url), without having to go through this intermediate.
I have tried to solve this using Saml2SecurityConfig, but I don't know how I have to set up it right.
How could it be done?
security:
saml2:
relyingparty:
registration:
company1:
identityprovider:
entity-id:
verification.credentials:
- certificate-location:
singlesignon.url: https://login.microsoftonline.com/XXXX/saml2
singlesignon.sign-request:
company2:
identityprovider:
entity-id:
verification.credentials:
- certificate-location:
singlesignon.url:
singlesignon.sign-request:
Saml2Config
public class Saml2Config extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
OpenSamlAuthenticationProvider authenticationProvider = new OpenSamlAuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
Saml2Authentication authentication = OpenSamlAuthenticationProvider
.createDefaultResponseAuthenticationConverter()
.convert(responseToken);
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
String username = assertion.getSubject().getNameID().getValue();
UserDetails userDetails = inMemoryUserDetailsManager().loadUserByUsername(username);
authentication.setDetails(userDetails);
return authentication;
});
http
.requestMatchers()
.antMatchers("/login/**","/saml2/**")
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.saml2Login().authenticationManager(new ProviderManager(authenticationProvider))
.and().csrf().disable();
}
}
I generally see organizations disambiguate their partners via the user's email address (like how the Office 365 Portal does it) or via a customer-specific FQDN (e.g., customer.service.com) similar to how Salesforce does it with their "My Domain" configuration.
If you use email address, then you should put that into the Subject of the AuthnRequest so that the IdP can use that in the login screen.

Choice between form login and OAuth2 login with Spring Security

I want to implement a simple app that enables users to log in with a local account or to register a new account or to login with OAuth2 - e.g. facebook. For the users which chose Facebook I would like to automatically create a local account and log them in with that account.
As far as I understand Spring Social is dead (it would be really helpful if this is mentioned on the home page of the project, because it would save efforts for people like me who invested in learning spring social).
The other thing that I understand is that "OAuth2 and OIDC are now first-class citizens in the Spring Boot and Spring Security ecosystems." Seems that the right way to go is to use Spring Security 5 with its first-class support of OAuth2!
So... let's go. My application.yaml:
spring:
security:
oauth2:
client:
registration:
facebook:
client-id: senko
client-secret: topsecret
The security configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
// for the ant pattern matcher syntax, please check:
// https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/AntPathMatcher.html
http
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login/authenticate")
.failureUrl("/login?param.error=bad_credentials")
.successForwardUrl("/home")
.and()
.logout()
.logoutUrl("/logout")
.deleteCookies("JSESSIONID").
and()
.authorizeRequests()
.antMatchers("/login**").permitAll()
.antMatchers("/**").authenticated().
and().
oauth2Login().
loginPage("/login");
}
#Override
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(
new BCryptPasswordEncoder());
}
My user service uses local storage backed by MySQL. The login form in short (thymeleaf):
Login:
<form id="signin" th:action="#{/login/authenticate}" method="post">
<input id="login" name="username" type="text" size="25"></input>
<input id="password" name="password" type="password" size="25"></input>
<button type="submit">Login In</button>
</form>
Or...:
<a th:href="#{/oauth2/authorization/facebook}">Sign in with Facebook</a>
So far I'm able to login with local account. I'm also able to login with Facebook. What I miss here is the part where I should create a local user account after the successful Facebook login. What is the correct way to implement that? I'm totally clueless. What I've tried so far is to search in google and to read the code of OAuth2LoginAuthenticationProvider. Any help will be appreciated.
UPDATE: I'm exploring if implementing an AuthenticationSuccessHandler is a proper option...
You can register a custom bean (OAuth2UserService) that will automatically replace the default configuration. Actually the example below just delegates to an implementation from the default configuration but allows to extend / add additional logic (in this example to process a user).
#Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
DefaultOAuth2UserService service = new DefaultOAuth2UserService();
return request -> {
OAuth2User user = service.loadUser(request);
System.out.println("User attributes: " + user.getAttributes()); // can be converted and saved
return user;
};
}

Spring boot Whitelabel page error for unauthorized ldap group access

I'm developing a web application in spring boot 2.0.0 and spring security 5.0.6 and I've had perfect luck with it until I added some logic that validates that the user is part of an LDAP group. If the user provides invalid credentials, the login page is shown again with a validation error displayed, but when the credentials are correct but the user is not part of the required LDAP group, the application is redirected to a Whitelabel Error Page that, along with that title in bold letters shows this:
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Fri Feb 22 18:40:55 EST 2019
There was an unexpected error (type=Forbidden, status=403).
Forbidden
This is the correct error, so I know the authentication is working. But I want to stay on the login page and not redirect, and I cannot figure out how to do that.
My entire configure method for my WebSecurityConfigurerAdapter is shown here below:
#Override
protected void configure(HttpSecurity http) throws Exception {
// Disabling CSRF because it causes issues with API requests (POSTs don't
// contain the CSRF tokens).
http.csrf().disable();
http.headers().frameOptions().disable();
http.authorizeRequests()
// This line turns off authentication for all management endpoints, which means all
// endpoints that start with "/actuator" (the default starter path for management endpoints
// in spring boot applications). To selectively choose which endpoints to exclude from authentication,
// use the EndpointRequest.to(String ... method, as in the following example:
// .requestMatchers(EndpointRequest.to("beans", "info", "health", "jolokia")).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
// Do not authenticate resource requests
.antMatchers(
"/app/css/**",
"/app/img/**",
"/app/js/**",
"/app/bootstrap/**").permitAll()
.antMatchers(
"/admin/**",
"/app/builds/**",
"/app/monitor/**",
"/app/review/**")
.hasRole(requiredRole)
// All other requests are authenticated
.anyRequest().authenticated()
// Any unauthenticated request is forwarded to the login page
.and()
.formLogin()
.loginPage(LOGIN_FORM)
.permitAll()
.successHandler(successHandler())
.and()
.exceptionHandling()
.authenticationEntryPoint(delegatingAuthenticationEntryPoint())
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_FORM))
.logoutSuccessUrl(LOGIN_FORM);
}
I'm open to critique of the construct of this entire method, btw--I've taken over this project and this is new to me. This code was working perfectly before I introduced the 6 lines ending with .hasRole(requiredRole), and it still works as long as the user is part of the required group.
I haven't provided the source for some of the methods that are called here, and I'm happy to paste them if someone would like. I'm guessing someone that knows this stuff well will spot the problem right away.
Any advice would be appreciated.
The solution I landed upon is similar to those I've found by googling around. I created an error controller that resolves to /error. At first I didn't think my error handler was ever being reached because I wasn't properly configured in my editor to debug a spring boot application, but I resolved that this morning. I added an error controller that looks like this:
#Controller
public class MyErrorController implements ErrorController {
private static final String ERROR_PATH = "/error";
#Autowired
private ErrorAttributes errorAttributes;
#RequestMapping(value = ERROR_PATH)
public String error(WebRequest request, HttpServletRequest httpServletRequest) {
Map<String, Object> attrs = this.errorAttributes.getErrorAttributes(request, false);
Integer status = (Integer) attrs.get("status");
if (status != null && status == 403) {
httpServletRequest.setAttribute("unauthorized", true);
}
return "/app/login";
}
#Override
public String getErrorPath() {
return ERROR_PATH;
}
}
So whenever an unauthorized attempt is made, this controller is hit and the condition of a status 403 triggers the logical branch in this method. I added the request attribute "unauthorized" and forward to the login jsp page, to which I've added a section that detects the presence of the request attribute. Voila, I'm all set.

custom login page for spring security oauth2 client login

I'm trying to implement social login in my spring boot 2 app with a custom login page. I place spring-boot-starter-oauth2-client as a dependency in my pom.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
I then provide some client id and secret details in my application properties file..
spring.security.oauth2.client.registration.google.client-id=client-id
spring.security.oauth2.client.registration.google.client-secret=client-secret
With this minimal code spring boot registers the client and provides a login page with a link to login via the client. Now I want to provide my own login page so I have created a Seurity config..
#Configuration
#EnableWebSecurity(debug = false)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/auth/**", "/login/oauth2/**", "/login**", "/logout", "/**/*.css", "/**/*.js").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login().loginPage("/login")
.and()
.logout();
}
}
I then provide a controller to serve up my custom login page, I have just put this in my spring boot config for now..
#SpringBootApplication
#Controller
public class SingleSignonApplication {
public static void main(String[] args) {
SpringApplication.run(SingleSignonApplication.class, args);
}
#RequestMapping("/login")
public String loginPage() {
return "login.html";
}
}
..and my login page (simplified) provides links to login via google..
<a class="btn btn-block btn-social btn-google" href="/oauth2/authorization/google"><span class="fab fa-google"></span> Sign in with Google</a>
Now when I sign in via this button, it calls google, is authenticated and then when its redirected to login/oauth2/code/google... I get a 404 page not found. Also /logout stops working. Do I need to manually configure the other stuff as I realise I am relying on spring boot to continue setting up the clients listed in the app propertie file and set up a client repository automatically.

Spring security login fail wrong redirect and no error message

I'm trying to build a simple login form (JS) and to user Spring security. As far as I understood, when login fails, it should redirect to login page (or is that only for JSP login pages inside bootstrap project?) but it fails do to that.
And query Error string parameter is also empty.
My spring security configuration:
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.defaultSuccessUrl("/", true)
.permitAll()
.and()
.httpBasic()
.and()
.csrf().disable()
.logout()
.permitAll()
.logoutSuccessUrl("/");
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(ImmutableList.of("*"));
configuration.setAllowedMethods(ImmutableList.of("HEAD",
"GET", "POST", "PUT", "DELETE", "PATCH"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(ImmutableList.of("Authorization", "Cache-Control", "Content-Type"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Bean
public UserDetailsService userDetailsService() {
// ensure the passwords are encoded properly
User.UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users.username("user").password("user").roles("USER").build());
manager.createUser(users.username("admin").password("admin").roles("USER","ADMIN").build());
return manager;
}
}
Boot:
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
From JS app I am sending a request to http://localhost:8080/login, I don't think it matters in this case, but I'm using MithrilJS request:
m.request({
method: "POST",
url: "http://localhost:8080/login",
body: {username: login, password: pass}
})
.then((result) => {
UserLogin.loggedIn = true;
})
.catch(error => {
console.log(error);
});
Responses (2 for some reason) I get:
http://localhost:8080/login?error
Request Method: OPTIONS
Response is empty
error string is also empty
http://localhost:8080/login?error
Request Method: GET
error String is empty
And now the funny part, response contains html (note that I don't have this HTML anywhere in my code):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Please sign in</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
</head>
<body>
<div class="container">
<form class="form-signin" method="post" action="/login">
<h2 class="form-signin-heading">Please sign in</h2>
<div class="alert alert-danger" role="alert">Invalid credentials</div> <p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
</p>
<p>
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
</p>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</body></html>
Any ideas where am I failing?
EDIT:
Thank your for the answers, while it did not answer exactly what I had in mind, it did lead me to right direction.
My main problem was that I have 2 separate projects: 1) a spring boot project 2) a JS application. JS application contains form html itself (or JS in this case) since I don't want any front end code to be or come from backend (spring boot project) while all the login logic is in spring boot spring security.
If I disable formLogin (which I have to do, no to use spring login form) I get no /login endpoint.
To summarize, I want to use spring security while bypassing spring login form (this way backend contains login logic, which can be accessed by any form, or that is the idea).
While I'm not quite there yet, I'm getting there.
For anyone that's curious, this is the article that helped: spring security without login form
You are trying to do authentication with ajax, so you can not redirect to any other page dependent on server response, you should do that in you JS(e.g. window.location.href).
Now let's talk about the form login in your case. The UsernamePasswordAuthenticationFilter is enabled based on your configuration.
.formLogin()
.defaultSuccessUrl("/", true)
.permitAll()
This filter will get username and password from the request params.
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
But you are trying to send a json body to the server, so it can not get the right credential. You should change it to a form request.
Next one is about the fail redirect url, now you should know the ajax can not redirect to an other page, the default failureHandler in you configuration will redirect to the login page with error, now you are using ajax, so you just can get the HTML, I think you can just validate the request based on the header(e.g. 401), here is an example.
.formLogin()
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
Here is the code in SimpleUrlAuthenticationFailureHandler
if (defaultFailureUrl == null) {
logger.debug("No failure URL set, sending 401 Unauthorized error");
response.sendError(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase());
}
You can get the result based on the header and the body.
Now I think your should know the defaultSuccessUrl in your configuration will not work as you expect. You need to implement you own AuthenticationSuccessHandler.
The last one is about your form authentication, the form authentication most of it is based on cookie, I think all your requests should contains the cookie to the server after login successfully. Maybe you can research JWT to instead.
The HTML is the default login form.
Why did you define formLogin()?
You must send username and password in the Authorization header not in the body.
From https://mithril.js.org/request.html
m.request({
method: "POST",
url: "http://localhost:8080/login",
user: login,
password: pass
})
.then((result) => {
UserLogin.loggedIn = true;
})
.catch(error => {
console.log(error);
});

Resources