Force User to Reenter Oauth Credentials After Logout | Spring Security - java

I have a Spring application that is secured using Oauth provided by Twitch. What I am trying to do is make it so that when the user clicks the logout button they have to reenter their Twitch credentials to log back into the site. From the reading I have done my understanding of the situation is that single sign off is fairly hard to achieve with Oauth. That said, Twitch's API seems to indicate that there is a way to tell them to invalidate an Oauth token: https://dev.twitch.tv/docs/authentication#revoking-access-tokens.
I have seen some information about a prompt=login parameter in OIDC but I have not been able to find any information about it or how to use it (on top of that I'm pretty sure that will require the users to reenter their credentials EVERY time rather than simply when they logout).
My initial approach to this problem was the number 1 answer in this thread but that did not actually change anything. I still was not required to enter my credentials upon trying to access one of the restricted endpoints, Spring simply quickly reauthenticated with Twitch and sent me through.
My current approach was to directly hit the endpoint in the Twitch api with a POST request (this was inspired by a comment in this thread). This approach is also not doing anything. I will include the code of that approach below.
I would much prefer to solve this problem with something built into Spring (holding out hope that there is something that I have somehow missed). An interesting thing I have been noticing happening is that when I hit /logout my browser will be redirected to Twitch's authorization endpoint which is making me think that for some reason Spring is trying to send a token revoke request to the authorization endpoint OR is logging me back into the endpoint as soon as I am logged out of it. Thought this was worth mentioning.
Any help replicating the initially mentioned behavior would be much appreciated.
SpringSecurityConfiguration:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#Configuration
#EnableWebSecurity
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity httpSecurity) throws Exception {
//This allows users to access the "/" and "/Info" endpoints without authenticating with Twitch. To go anywhere else they will have to authenticate.
httpSecurity.antMatcher("/**").authorizeRequests().antMatchers("/", "/Info", "/token/deletion").permitAll().anyRequest().authenticated().and().oauth2Login().and()
//This configures logout tells spring to do the logout with the method in the logoutSuccessHandler
.logout().logoutSuccessUrl("http://localhost:8080/token/deletion").invalidateHttpSession(true).clearAuthentication(true).deleteCookies("JSESSIONID", "JWT");
}
application.properties:
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
spring.security.oauth2.client.registration.twitch=twitch
spring.security.oauth2.client.registration.twitch.client-id=redacted
spring.security.oauth2.client.registration.twitch.client-secret=redacted
spring.security.oauth2.client.registration.twitch.client-authentication-method=post
spring.security.oauth2.client.registration.twitch.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.twitch.redirect-uri=http://localhost:8080/login/oauth2/code/twitch
spring.security.oauth2.client.registration.twitch.scope=user:read:email
spring.security.oauth2.client.registration.twitch.client-name=Twitch
spring.security.oauth2.client.provider.twitch.authorization-uri=https://id.twitch.tv/oauth2/authorize
spring.security.oauth2.client.provider.twitch.token-uri=https://id.twitch.tv/oauth2/token
spring.security.oauth2.client.provider.twitch.jwk-set-uri=https://id.twitch.tv/oauth2/keys
spring.security.oauth2.client.provider.twitch.user-info-uri=https://id.twitch.tv/oauth2/userinfo
spring.security.oauth2.client.provider.twitch.user-info-authentication-method=post
spring.security.oauth2.client.provider.twitch.user-name-attribute=sub
TokenRemovalController:
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.net.ssl.HttpsURLConnection;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
#Controller
public class TokenRemovalController {
#RequestMapping("/token/deletion")
public void removeTokenFromTwitch(HttpServletResponse response, #RegisteredOAuth2AuthorizedClient("twitch") OAuth2AuthorizedClient authorizedClient) throws IOException {
//Get access token of current user
String accessToken = authorizedClient.getAccessToken().getTokenValue();
//POST Request to Twitch endpoint
URL url = new URL("https://id.twitch.tv/oauth2/revoke");
HttpsURLConnection https = (HttpsURLConnection)url.openConnection();
https.setRequestMethod("POST");
https.setDoOutput(true);
https.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
String data = "client_id=redacted&token=" + accessToken;
byte[] out = data.getBytes(StandardCharsets.UTF_8);
OutputStream stream = https.getOutputStream();
stream.write(out);
String redirectString = "/error";
if (https.getResponseCode() == 200) {
redirectString = "/Info";
}
https.disconnect();
response.sendRedirect(redirectString);
}
}
Project Structure:
Another thing I forgot to mention was that if I delete all of my browsers cookies then I do get logged out of my site. Is it potentially possible to replicate this effect when users attempt to log out (obviously without deleting all of the users other cookies)?

Related

Spring-security 3.1 single sign on LDAP

I'm trying to add single sign on to two legacy systems on different domains. That currently have working "regular" login.
I found this https://stackoverflow.com/a/9925146 but I'm unsure about the step 1 more specifically this
"implement functionality to serialize and write the Authentication object to a Session cookie with a global scope."
If I understand this correctly I should extract the sessionID and add it to a new cookie with a global scope.
I started by trying to extract the sessionID like so
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException,ServletException {
Cookie cookie = null;
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
if (authentication.getDetails() != null) {
WebAuthenticationDetails dets = (WebAuthenticationDetails) auth.getDetails();
System.out.println("sessionID: " + dets.getSessionId());
}
response.addCookie(cookie);
super.onAuthenticationSuccess(request,response,authentication);
}
}
To verify that I'm on the right track i print the sessionID to terminal and compare it to the sessionID that spring-security sets in the browser. If I understand correctly they should match. They don't match. Am I misunderstanding the solution suggested in the answer?
Single sign on is a very difficult problem to get right. I really would not recommend attempting to implement it unless you have a good grasp of the problem and how to solve it. If you can I highly recommend you try to use Oauth2 instead of implementing it yourself.
https://www.baeldung.com/sso-spring-security-oauth2 might give you a starting point.
If you are using an application server like JBoss or WebSphere you may be able to use their SSO option instead.

Keycloak: Making custom UserModel implementation work for OIDC

What I want to do:
Make Keycloak work as authentication provider using OIDC, for users provided by a custom User Storage SPI implementation.
What I have done so far:
Setup Keycloak 8.0.1 (realm, client, "native" test users, etc.)
Setup a small Spring Boot App (Spring Security without the Keycloak Adapter) as test client
Implement and install a custom User Storage SPI, following this guide https://www.keycloak.org/docs/8.0/server_development/#simple-read-only-lookup-example
I took small liberties to keep it more extendable (for example, creating an actual class for the UserModel), but that shouldn't influence the functional bits.
What works:
Authentication (Authorization Grant Flow) in the client app for "native" Keycloak users
What doesn't work:
Authentication in the client app for users provided through the User Storage SPI
From what I can tell, the failure occurs when the access token is created. Login, code creation and redirect happen.
I assume the problem is that my UserModel implementation does not provide all needed attributes to create a token. Could be something different...
Here is what it looks like:
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.storage.adapter.AbstractUserAdapter;
import de.dfb.keycloak.spi.userstorage.service.DfbUserDto;
public class DfbUser extends AbstractUserAdapter {
private String username;
public DfbUser(KeycloakSession session, RealmModel realm, ComponentModel storageProviderModel, DfbUserDto userDto) {
super(session, realm, storageProviderModel);
this.username = userDto.getUsername();
}
#Override
public String getUsername() {
return username;
}
// TODO Only implemented to avoid this problem
// https://issues.redhat.com/browse/KEYCLOAK-6115
#Override
public void setSingleAttribute(String name, String value) {
if (!name.equals(LOCALE)) {
super.setSingleAttribute(name, value);
}
}
...
}
What I want to know:
How do I make this work? If it's just missing user attributes: Which ones am I missing? What should they look like?
Are there more resources available, to get a better grasp of keycloaks data model, to avoid similar problems? The developer docs do not seem quite extensive enough and the java doc is missing a bunch of commentary.

Spring boot oauth callback for google

I am sharing the complete code regards to my problem.
I am taking the social configuration file handled via social-cfg.xml it is in classpath.
social-cfg.xml
google.client.id=#####################################################################
google.client.secret=############################
google.scope=https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile
I have made java handler to handle this config file
socialConfig.java
package com.inno.config;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.UserIdSource;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
import org.springframework.social.config.annotation.EnableSocial;
import org.springframework.social.config.annotation.SocialConfigurer;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.ConnectionRepository;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;
import org.springframework.social.connect.web.ConnectController;
import org.springframework.social.google.connect.GoogleConnectionFactory;
import org.springframework.social.security.AuthenticationNameUserIdSource;
import com.inno.dao.AppUserDAO;
import com.inno.service.ConnectionSignUpImpl;
#Configuration
#EnableSocial
// Load to Environment.
#PropertySource("classpath:social-cfg.properties")
public class SocialConfig implements SocialConfigurer {
private boolean autoSignUp = false;
#Autowired
private DataSource dataSource;
#Autowired
private AppUserDAO appUserDAO;
#Override
public void addConnectionFactories(ConnectionFactoryConfigurer cfConfig, Environment env) {
try {
this.autoSignUp = Boolean.parseBoolean(env.getProperty("social.auto-signup"));
} catch (Exception e) {
this.autoSignUp = false;
}
// Google
GoogleConnectionFactory gfactory = new GoogleConnectionFactory(//
env.getProperty("google.client.id"), //
env.getProperty("google.client.secret"));
gfactory.setScope(env.getProperty("google.scope"));
cfConfig.addConnectionFactory(gfactory);
}
//............. more code regards to another functionality.
when i am hitting this href
<a th:href="#{/auth/google}">Google</a>
<br />
Then it's redirecting to google page with error as below
Error: redirect_uri_mismatch
The redirect URI in the request, http://localhost:8787/auth/google, does not match the ones authorized for the OAuth client. To update the authorized redirect URIs, visit: https://console.developers.google.com/apis/credentials/oauthclient/#######?project=#######
when i visit https://console.developers.google.com/apis/credentials/oauthclient/#######?project=#######
there are two option's
Authorized JavaScript origins
Authorized redirect URIs
I have set both of them to validate with my webapp. I just don't where is the point which stuck out this issue. PLease help me out. Also ask for any other requirement if required.
redirect URI in the google developer console must exactly match where you are sending your request from. In your case the error message tells you exactly where you are sending your request form.
http://localhost:8787/auth/google
Which means that you need to have that exactly in the Google developer console under Authorized redirect URIs or the authentication sever will not accept your request.
Note Port must be included. Exactly means exactly with the exception of cased.

Restricting Access to method in Play Framework with Authorization - Java

I am having trouble grasping the idea of authorization in PlayFramework (version 2.5). My situation is I have a REST API method getUser and I want to restrict its access by performing authorization with a token that is coming in custom request header named "X-Authorization". Now my controller code looks like that:
package controllers;
import models.User;
import org.bson.types.ObjectId;
import play.mvc.*;
import org.json.simple.*;
import views.html.*;
public class ApiController extends Controller {
public Result getUser(String userId) {
User user = User.findById(new ObjectId(userId));
JSONObject userG = new JSONObject();
//Some code to append data to userG before return
return ok(userG.toJSONString());
}
}
The route URL is defined like this:
GET /api/user/:id controllers.ApiController.getUser(id)
Option 1 could be to check the Authorization token inside the method getUser and also check for other credentials but I want to restrict access before even it get calls getUser method. As in future I will be adding more method calls to this REST API. So I will be reusing the same authorization to those future REST APIs as well.
I found there is authorization available in Play Framework which I am not able to understand. I tried to implement Authorization by extending class Security.Authenticator and overriding methods getUserName and onUnauthorized like this:
package controllers;
import models.Site;
import models.User;
import org.json.simple.JSONObject;
import play.mvc.Http.Context;
import play.mvc.Result;
import play.mvc.Security;
public class Secured extends Security.Authenticator {
#Override
public String getUsername(Context ctx) {
String auth_key = ctx.request().getHeader("X-Authorization");
Site site = Site.fineByAccessKey(auth_key);
if (site != null && auth_key.equals(site.access_key)) {
return auth_key;
} else {
return null;
}
}
#Override
public Result onUnauthorized(Context ctx) {
JSONObject errorAuth = new JSONObject();
errorAuth.put("status", "error");
errorAuth.put("msg", "You are not authorized to access the API");
return unauthorized(errorAuth.toJSONString());
}
}
Then I've appended the annotation to the getUser method with #Security.Authenticated(Secured.class). It works fine and returns unauthorized error. But now I am not sure if that is the preferred way. I feel this is not the right way to do it as the name of the override of function getUsername suggests that too. I am not checking for any username in session or cookie rather only the token present in the header of request.
Also I know there is a module named Deadbolt which is used for authorization but I read its documents and I am not able to integrate it. It was relatively complex integration for a beginner like me. I was confused about how to use it. I thought about using SubjectPresent controller authorization but still I was not able to implement it successfully.
In the end what do you guys suggest that should I use Security.Authenticator the way I have implemented? Or do you suggest that I go to my first option that is checking authorization inside getUser method? Or Anyone can tell me how to implement Deadbolt in my scenario?
You are mixing Authorization and Authentication.
Here is a good thread: Authentication versus Authorization
I like this answer:
Authentication = login + password (who you are)
Authorization = permissions (what you are allowed to do)
Authentication == Authorization (excluding anonymous user) if you allow doing something for all users that you know (i.e. Authenticated users)
The main goal of Deadbolt is Authorization (already Authenticated users). Your main goal is Authentication.
I would advise you to use Pac4J, it Authentication library not only for Play, and it has versions as for Java as for Scala. There is a good sample project: https://github.com/pac4j/play-pac4j-java-demo
I use this library myself in my projects and the task
As in future i will be adding more method calls to this REST api. So i
will be reusing the same authorization to those future REST apis as
well.
I solve as easy as just add the configuration in the 'application.conf`:
pac4j.security {
rules = [
{"/admin/.*" = {
authorizers = "ADMIN"
clients = "FormClient"
}}
]
}
Just do not forget to add Security filter. This feature present in the example project, so just clone and try.
Another example form the official page:
pac4j.security.rules = [
# Admin pages need a special authorizer and do not support login via Twitter.
{"/admin/.*" = {
authorizers = "admin"
clients = "FormClient"
}}
# Rules for the REST services. These don't specify a client and will return 401
# when not authenticated.
{"/restservices/.*" = {
authorizers = "_authenticated_"
}}
# The login page needs to be publicly accessible.
{"/login.html" = {
authorizers = "_anonymous_"
}}
# 'Catch all' rule to make sure the whole application stays secure.
{".*" = {
authorizers = "_authenticated_"
clients = "FormClient,TwitterClient"
}}
]

Spring Security - Multiple Sessions depending on usage (login/httpBasic)

I have a project where I have one application "a" with rest services and another one ("b") for a web page where the users can login and use 1 or 2 rest services of "a" (e.g. posting a comment).
The problem is, the rest services are all secured and shall authorize via httpBasic every time (no session).
The services needed for "b" on the other hand shall have a session.
so:
requests with the rest services directly -> httpBasic everytime(no session)
login via webpage/webpage handling requests -> session via login
Is there a way to achieve this? My only solution right now would be to delete the session after the httpBasic process, but I don't know how to do that.
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.authorizeRequests().anyRequest().permitAll()
.and().httpBasic();
If you really wish to delete session data it can be achieved by implementing LogoutSuccessHandler interface.
package com.arjun.security;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Service;
import com.informage.arnav.domain.CassandraLoginSession;
#Service
public class AppLogoutSuccessHandler implements LogoutSuccessHandler {
#Autowired
private CassandraLoginSessionDao cassandraLoginSessionDao;
public AppLogoutSuccessHandler() {
super();
}
#Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
if (authentication != null) {
final Object principal = authentication.getPrincipal();
final AppUserDetails appUserDetails;
if (principal != null && principal instanceof AppUserDetails) {
appUserDetails = (AppUserDetails) principal;
CassandraLoginSession cassandraLoginSession=cassandraLoginSessionDao.findBySessionId(appUserDetails.getSessionId().toString());
//can also delete based on userId
//CassandraLoginSession cassandraLoginSession=cassandraLoginSessionDao.findByUserId(appUserDetails.getUserId());
cassandraLoginSessionDao.hardDelete(cassandraLoginSession);
//delete the session data from DB where session is stored
}
}
}
}
In your security configuration you need to configure this handler.
<logout logout-url="/logout" success-handler-ref="appLogoutSuccessHandler" />
You can write following to permit access to your webservice without login:
http.authorizeRequests()
.antMatchers("/webserviceNotRequiredAuthorization*").permitAll();
Then you must write following to which you webservices need authorization :
http.authorizeRequests().antMatchers("/webserviceRequiredAuthorization").hasAnyRole("ROLE_ADMIN");
in short first write the URL pattern for which authorization is not required and then write the URL pattern for which authorization is required.
http.authorizeRequests()
.antMatchers("/webserviceNotRequiredAuthorization*").permitAll();
http.authorizeRequests().antMatchers("/webserviceRequiredAuthorization").hasAnyRole("ROLE_ADMIN");

Categories

Resources