I have a Java web application which do SPNEGO authentication of clients in a Windows Active Directory environment.
To authenticate the user we use code from the good old SPNEGO SourceForge project.
String encodedAuthToken = (String) credentials;
LOG.debug("Encoded auth token: " + encodedAuthToken);
byte[] authToken = B64Code.decode(encodedAuthToken);
GSSManager manager = GSSManager.getInstance();
try {
Oid krb5Oid = new Oid("1.3.6.1.5.5.2");
GSSName gssName = manager.createName(_targetName, null);
GSSCredential serverCreds = manager.createCredential(gssName, GSSCredential.INDEFINITE_LIFETIME, krb5Oid, GSSCredential.INITIATE_AND_ACCEPT);
GSSContext gContext = manager.createContext(serverCreds);
if (gContext != null) {
while (!gContext.isEstablished()) {
authToken = gContext.acceptSecContext(authToken, 0, authToken.length);
}
if (gContext.isEstablished()) {
// Login succeeded!
String clientName = gContext.getSrcName().toString();
}
}
}
The authentication works good but we also have a requirement to delegate the user credentials to a back-end service (Exchange EWS), using constrained delegation.
When configuring this in our AD it looks like a small difference, but it's not. See:
AD delegation settings
The difference is described here: msdn.microsoft.com/en-us/library/cc246080.aspx?f=255&MSPPError=-2147217396
With unconstrained delegation we could simply use the available delegated credentials when we call the back-end service and it would all be good:
GSSCredential delegatedCreds = gContext.getDelegCred()
SpnegoHttpURLConnection conn = new SpnegoHttpURLConnection(clientCreds);
With constrained delegation we have no access to the users TGT and it seems we need to use the MS-SFU (S4U2proxy) Kerberos extension which Java 8 is suppose to support.
The only example I could find is this one: https://github.com/ymartin59/java-kerberos-sfudemo (thanks Yves Martin for that!)
Now to my problem... After my authentication I basically end up with the username of the authenticated user (see "clientName" in code above).
Do we really need to use the S4U2self mechanism to impersonate the user here?
The client just sent us it's Kerberos Service Ticket (wrapped in the SPNEGO token I can't decode).
Ideally we should be able to use that service ticket and my own service's TGT to authenticate the user (using the S4U2proxy mechanism)?
But I do not understand how.
So now I'm wondering if it's possible to tie together our SPNEGO authentication with S4U2proxy delegation?
Many thanks for any input on this.
I've actually been doing something like this recently but am using spring security kerberos. I put an example on github here. The key thing that I found that I needed set up to use constrained delegation like you want it and S4U2Proxy was to make sure (if you're using Oracle/OpenJDK) you set isInitiator=true in your JAAS Config so that when getDelegCred is called you get back a Krb5ProxyCredential. See comment here. With that credential, you can use it to create service ticket tokens on the Users behalf for the services you are constrained to use in the normal fashion, like this.
I've done a lot of investigation on Kerberos constrained delegation, and finally I've figured out the correct way of doing it using Java.
Settings on Domain Controller
1) No Delegation: Do not trust this account for delegation
You (service user) can not get delegated credentials of the user. It means you can not perform any task on end user's behalf.
At the most you can do is to accept the incoming ticket from the user(usually browser) and get it verified by passing it to KDC. In response, KDC will tell you for which user(or principal) this ticket is issued to, but no credentials will be passed.
2) Unconstrained Delegation: Trust this account for delegation to any service (Kerberos only)
With this option, you (service user) get the delegated credentials of the user. Moreover, what you get is a TGT of the user. Using this TGT, you can request TGS (service ticket) on user's behalf for any service.
3) Trust this account for delegation to specified services (Kerberos only)
Here, you specify the services to which you can use the delegated credentials. It means when this option is enabled, you get the delegated credentials, however, you are allowed to use them only to get end user's TGS for the specified services.
Another important point is, you must have end user's TGS (end user's TGS for your web app). Then using this TGS, you can request KDC the end user's TGS for another service.
4) Trust this account for delegation to specified services (Any Protocol)
This is also known as protocol transition. In this option also, you need to specify the services for which you can request the TGS to KDC on user's behalf.
You (service user) are allowed to 'impersonate' the end user, without having any kind of ticket from the end user. You can impersonate any user, and get TGS for the specified services.
This option is useful for backgroung processes or schedulars where end user interaction is not possible.
Java Code Samples
1) Getting Delegated Credentials (useful in option 2 and 3 stated above)
// ---------------------------------
// step 1: Login using service user credentials and get its TGT
// ---------------------------------
Subject subject = new Subject();
Krb5LoginModule krb5LoginModule = new Krb5LoginModule();
Map<String,String> optionMap = new HashMap<String,String>();
optionMap.put("keyTab", "c:\\ticket\\sapuser.keytab");
optionMap.put("principal", "HTTP/TEST"); // SPN you mapped to the service user while creating the keytab file
optionMap.put("doNotPrompt", "true");
optionMap.put("refreshKrb5Config", "true");
optionMap.put("useTicketCache", "true");
optionMap.put("renewTGT", "true");
optionMap.put("useKeyTab", "true");
optionMap.put("storeKey", "true");
optionMap.put("isInitiator", "true"); // needed for delegation
optionMap.put("debug", "true"); // trace will be printed on console
krb5LoginModule.initialize(subject, null, new HashMap<String,String>(), optionMap);
krb5LoginModule.login();
krb5LoginModule.commit();
// ---------------------------------
// Step 2: Use login context of this service user, accept the kerberos token (TGS) coming from end user
// ---------------------------------
public GSSCredential validateTicket(byte[] token) {
try {
return Subject.doAs(this.serviceSubject, new KerberosValidateAction(token));
}
catch (PrivilegedActionException e) {
throw new BadCredentialsException("Kerberos validation not successful", e);
}
}
private class KerberosValidateAction implements PrivilegedExceptionAction<GSSCredential> {
byte[] kerberosTicket;
public KerberosValidateAction(byte[] kerberosTicket) {
this.kerberosTicket = kerberosTicket;
}
#Override
public GSSCredential run() throws Exception {
byte[] responseToken = new byte[0];
GSSName gssName = null;
GSSContext context = GSSManager.getInstance().createContext((GSSCredential) null);
while (!context.isEstablished()) {
responseToken = context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length);
gssName = context.getSrcName();
if (gssName == null) {
throw new BadCredentialsException("GSSContext name of the context initiator is null");
}
}
//check if the credentials can be delegated
if (!context.getCredDelegState()) {
SecurityLogger.getLogger().error("Credentials can not be delegated. Please make sure that delegation is enabled for the service user. This may cause failures while creating Kerberized application.");
return null;
}
// only accepts the delegated credentials from the calling peer
GSSCredential clientCred = context.getDelegCred(); // in case of Unconstrained Delegation, you get the end user's TGT, otherwise TGS only
return clientCred;
}
}
// ---------------------------------
// Step 3: Initiate TGS request for another service using delegated credentials obtained in previous step
// ---------------------------------
private Object getServiceTicket(GSSCredential clientCred) throws PrivilegedActionException {
Object o = Subject.doAs(new Subject(), (PrivilegedExceptionAction<Object>) () -> {
GSSManager manager = GSSManager.getInstance();
Oid SPNEGO_OID = new Oid("1.3.6.1.5.5.2");
Oid KRB5_PRINCIPAL_OID = new Oid("1.2.840.113554.1.2.2.1");
GSSName servicePrincipal = manager.createName("HTTP/TEST", KRB5_PRINCIPAL_OID); // service to which the service user is allowed to delegate credentials
ExtendedGSSContext extendedContext = (ExtendedGSSContext) manager.createContext(servicePrincipal, SPNEGO_OID, clientCred, GSSContext.DEFAULT_LIFETIME);
extendedContext.requestCredDeleg(true);
byte[] token = new byte[0];
token = extendedContext.initSecContext(token, 0, token.length); // this token is the end user's TGS for "HTTP/TEST" service, you can pass this to the actual HTTP/TEST service endpoint in "Authorization" header.
return token;
});
return o;
}
2) Getting Impersonated Credentials (useful in option 4 stated above)
Initial steps are similar as mentioned inside step 1 above. You need to login using service user credentials. There is small change in 'run' method, which is given below:
#Override
public GSSCredential run() throws Exception {
GSSName gssName = null;
GSSManager manager = GSSManager.getInstance();
GSSCredential serviceCredentials = manager.createCredential(GSSCredential.INITIATE_ONLY);
GSSName other = manager.createName("bhushan", GSSName.NT_USER_NAME, kerberosOid); // any existing user
GSSCredential impersonatedCredentials = ((ExtendedGSSCredential) serviceCredentials).impersonate(other);
return impersonatedCredentials;
}
}
You can see that we don't need user's TGS in this case.Getting TGS on user's behalf for other service, is same as mentioned in step 3 given in above code. Just pass these impersonatedCredentials instead of delegatedCredentials.
I hope this will be helpful.
Thanks,
Bhushan
Related
I've been following this tutorial in order to create an Authentication Server, but I'm facing some problems regarding the concepts, I guess.
Look, when I register a Client in Repository, I have to define some parameters, like its id, secret, authentication method, grant types, redirection uris and scopes:
#Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("articles-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc")
.redirectUri("http://127.0.0.1:8080/authorized")
.scope(OidcScopes.OPENID)
.scope("articles.read")
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
When I'm back to my Resource Server, I find that my client was successfully logged in and it returns with an "articles.read" scope. Everything is fine here, supposing that I want to protect my endpoints with the Client's scope, but this is not my case.
In my situation, I want to protect my endpoints according to my User's role in database.
I'll give you an example, so you don't have to read the whole Baeldung's website:
I try to access: http://localhost:8080/articles.
It redirects to: http://auth-server:9000, where a Spring Security Login Form appears.
When you submit the proper credentials (which are compared from a database using the default Spring Security schema), it basically gets you back to: http://localhost:8080/articles.
Well, in that point, I have an Authorization Token with the Client scope, but not the logged User role.
Is there an standard way to configure my project to achieve this or, do I have to think of a creative way to do so?
Thank you in advance.
For role based authentication you should map authorities in Oauth token.
OAuth2AuthenticationToken.getAuthorities() is used for authorizing requests, such as in hasRole('USER') or hasRole('ADMIN').
For this you need to implement the userAuthoritiesMapper, something like this:
#Configuration
public class AppConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login().userInfoEndpoint().userAuthoritiesMapper(this.userAuthoritiesMapper());
//.oidcUserService(this.oidcUserService());
super.configure(http);
}
private GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (OidcUserAuthority.class.isInstance(authority)) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;
OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
if (userInfo.containsClaim("role")){
String roleName = "ROLE_" + userInfo.getClaimAsString("role");
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
} else if (OAuth2UserAuthority.class.isInstance(authority)) {
OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (userAttributes.containsKey("role")){
String roleName = "ROLE_" + (String)userAttributes.get("role");
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
}
});
return mappedAuthorities;
};
}
}
I've got the following set up:
Central auth server written with spring boot that is currently working (I can curl and receive an access token, jdbc token store, etc)
Multiple applications owned by the same developer, sharing the same customer base on different domains. IE: John Doe for app1 is the same as John Doe for app2.
I have an existing application (app1 above) that is jsf 2.2 with spring security configured for login purposes. That application works stand alone right now, with it's own user base.
This is the flow I am trying to obtain:
Resource Owner Password Credential OAuth Flow
So we would want:
User goes to app1
User enters user and password into app1 login page
User hits "login"
Some sort of configuration in Spring would then take the loginByUsername request, get access token from the central oauth server
We now have app1 access - the user could have one of three roles (ADMIN, USER, SUPERUSER).
When they go to (say) app1/views/createEntry.xhtml, we would confirm the access token we currently have is still active on the auth server.
The resource server would technically be the resources on the app1 server (right?)
I'm new to this oauth2.0 process (and spring really), but I think this is the flow I want. How do I set this up with Spring Security? I've seen a security setting called oauth2login() that I think is what we COULD want, but I think that is more authorization code flow.
I haven't found a very good example of this using the password flow.
I do trust each of the applications in this process, hence the password flow. We control the network that maintains traffic between the auth server and the other applications.
Edit: SSO isn't an option because of requirements and our customer base. The applications are unique enough that it doesn't make sense, but the user should be able to log into any of our applications with those credentials.
Edit 2: Sorry for second edit. I would like to add that I've added a resource configuration on app1 and it actually seems like it works - I've secured anything /views/* and when I attempt to go their, I get the expected "Full Authentication required" message.
Edit 3: I think I am making some progress -
First, I created a spring component that implements AuthenticationProvider and then overwrote the authenticate method so that I created a ResourceOwnerPasswordResourceDetails object with all my properties (client id, client secret, grant type, scope, etc) and called the authorization server to get a token. My excitement to see my log refresh for the authorization server was high.
Next step I need to figure out is how to generate an extension of org.springframework.security.core.userdetails.User so that I can store the privileges for the user.
Also - I can't quite figure out yet how the token is stored. I know the auth server generates a token and stores in jdbc, but where/how does the token get stored on the client side?
For those that were curious, here is how I set up the authentication provider on my client (app1). I still have issues with the resource server (ill ask a separate question), but here is what I did:
Custom authenticator:
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
private AppUserDAO appUserDAO;
private String accessTokenUri = "http://localhost:8080/oauth/token";
private String clientId = "clientid";
private String clientSecret = "clientsecret";
public AccessTokenProvider userAccessTokenProvider() {
ResourceOwnerPasswordAccessTokenProvider accessTokenProvider = new ResourceOwnerPasswordAccessTokenProvider();
return accessTokenProvider;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
final String username = authentication.getName();
final String password = authentication.getCredentials().toString();
List<String> scopes = new ArrayList<String>();
scopes.add("read");
final ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
resource.setUsername(username);
resource.setPassword(password);
resource.setAccessTokenUri(accessTokenUri);
resource.setClientId(clientId);
resource.setClientSecret(clientSecret);
resource.setGrantType("password");
resource.setScope(scopes);
// Generate an access token
final OAuth2RestTemplate template = new OAuth2RestTemplate(resource, new DefaultOAuth2ClientContext(new DefaultAccessTokenRequest()));
template.setAccessTokenProvider(userAccessTokenProvider());
OAuth2AccessToken accessToken = null;
try {
accessToken = template.getAccessToken();
System.out.println("Grabbed access token from " + accessTokenUri);
}
catch (OAuth2AccessDeniedException e) {
if (e.getCause() instanceof ResourceAccessException) {
final String errorMessage = String.format(
"While authenticating user '%s': " + "Unable to access accessTokenUri '%s'.", username,
accessTokenUri);
throw new AuthenticationServiceException(errorMessage, e);
}
throw new BadCredentialsException(String.format("Access denied for user '%s'.", username), e);
}
catch (OAuth2Exception e) {
throw new AuthenticationServiceException(
String.format("Unable to perform OAuth authentication for user '%s'.", username), e);
}
// Determine roles for user
List<GrantedAuthority> grantList = ...
// Create custom user for the principal
User user = .....
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, null /*dont store password*/, grantList);
return token;
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
Security configuration:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider authProvider;
....
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
}
I'm trying to solve a puzzle with enabling OAuth2-based authentication for my Feign client that is used for cross-service communication.
In normal cases, when a user pushes a request through API, I'm able to take all authentication details needed from the Spring's SecurityContextHolder (as it normally does its job and fills all the details objects) and enhance Feign's Request as follows:
public class FeignAccessTokenRelyInterceptor implements RequestInterceptor {
private static final Logger log = LoggerFactory.getLogger(FeignAccessTokenRelyInterceptor.class);
#Override
public void apply(RequestTemplate template) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null){
String tokenValue = null;
if (auth.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails();
tokenValue = details.getTokenValue();
}
if (tokenValue == null) {
log.warn("Current token value is null");
return;
}
template.header("Authorization", "Bearer " + tokenValue);
}
}
}
However, when it comes to scheduled calls that are triggered inside the system, the SecurityContext is obviously empty. I'm filling it with UserInfoTokenServices by manually requesting the access token by client credentials flow beforehand and loading user details:
OAuth2Authentication authentication = userInfoTokenServices.loadAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
But such construction doesn't fill OAuth2Authentication.details, on which I rely to get an access token. I tried extending OAuth2AuthenticationDetails, but the only constructor requires HttpServletRequest which is hard to get inside a scheduled task, and making a dummy instance of it feels like a bad choice.
So far, I see the only adequate option to make a separate custom implementation of details holder and pass it to OAuth2Authentication along with the access token I have. And then pick it up in FeignAccessTokenRelyInterceptor.
The question
Maybe there are some other options where I can store my access token in the security context and reliably get it from there, in order not to produce new custom classes?
Will be glad for any help.
Some related links I've studied:
How to get custom user info from OAuth2 authorization server /user endpoint
Spring Boot / Spring Cloud / Spring Security: How to correctly obtain an OAuth2 Access Token in a scheduled task
Spring #FeignClient , OAuth2 and #Scheduled not working
How can I authenticate a system user for scheduled processes in Spring?
For the history, hope that'd help someone like me struggling with that.
I didn't find a better way than the initial one and made a custom InternalOAuth2Details to hold a token value obtained from Spring's OAuth services. Then, in the FeignAccessTokenRelyInterceptor I simply check if current details are InternalOAuth2Details and try to get a token value from it, as follows:
#Override
public void apply(RequestTemplate template) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null){
String tokenValue = null;
if (auth.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails();
tokenValue = details.getTokenValue();
} else if (auth.getDetails() instanceof InternalOAuth2Details) {
InternalOAuth2Details details = (InternalOAuth2Details) auth.getDetails();
tokenValue = details.getTokenValue();
}
if (tokenValue == null) {
log.warn("Current token value is null");
return;
}
template.header("Authorization", "Bearer " + tokenValue);
}
}
I bet it isn't the best solution, but it seems to work quite stable as of now.
I've been using Google OAuth to let users authorize access to the Calendar Service for my Web Application. After a successful 3-legged auth flow, I was storing all user's credentials in a common file on the app Server. The next time the app needs to use the service, it will check if the credentials exist, and if yes, it will assume they are valid
code works like that
#Override
public void _authorize(String userId) throws IOException {
// Check if user has already authorised the service.
Credential credents = flow.loadCredential(userId);
// Checking if the given user is not authorized
if (credents == null) {
//Create credentials now. user will be redirected to authorise
try {
//Creating a LocalServer Receiver
// Getting the redirect URI
// Creating a new authorization URL
// Setting the redirect URI
// Building the authorization URL
// Receiving authorization code
// Exchanging it for an access token
// Storing the credentials for later access
credents = flow.createAndStoreCredential(response, id);
} finally {
// Releasing resources
}
} else {
// Assume the credentials are valid. so there's nothing left to do here, let's get that client
//Update: Nooooooot! the user might have revoked the authorization, so credents != null BUT they are invalid
//TODO: handle an Exception here, and manage the revoked credentials
}
// Setting up the calendar service client
client = new com.google.api.services.calendar.Calendar.Builder(httpTransport, jsonFactory, credents).setApplicationName(APPLICATION_NAME)
.build();
}
This works fine, as long as the user never changes his mind. But if the user decides to manually revoke the authorization using the Google Account security options, the com.google.api.services.calendar.Calendar retrieval will Fail.
My question is :
Is there a way to check if the credentials are still valid, before trying to use them ?
Else, I can only guess that the failure to get the client object, is the only way to have my portal realize that the credentials are no more valid ?
What should I do about the invalid/revoked credentials ? should I just call flow.createAndStoreCredential and they are going to be overwritten? Or do I have to delete the old ones first ? (how ?)
You can use the refreshToken() method for this. See example:
// Fetch credential using the GoogleAuthorizationCodeFlow
GoogleAuthorizationCodeFlow authorizationCodeFlow;
Credential credential = authorizationCodeFlow.loadCredential(userId);
if (credential != null) {
try {
// refresh the credential to see if the refresh token is still valid
credential.refreshToken();
System.out.println("Refreshed: expires in: " + credential.getExpiresInSeconds());
} catch (TokenResponseException e) {
// process exception here.
// This will catch the Exception.
// This Exception contains the HTTP status and reason etc.
// In case of a revoke, this will throw something like a 401 - "invalid_grant"
return;
}
} else {
// No credential yet known.
// Flow for creating a new credential here
}
EDIT
If you indeed have an invalid refresh token and you want to renew it, then you need to repeat the steps that you did in the first place to get the credentials. So:
genererate a new authorization URL
redirect the user to it
user accepts the consent screen
catch the authorization code from the redirect back to your app
request a new token from Google using the authorization code
create and store a new Credential using the response from Google
No need to delete the old credential. But if you want to explicitly do so, it is possible.
Something like:
// This userId is obviously the same as you used to create the credential
String userId = "john.doe";
authorizationCodeFlow.getDataStore().delete(userId);
You can use the endpoint https://www.googleapis.com/oauth2/v1/tokeninfo to determine if an OAuth2 token is still valid. More information is available in the OAuth2 guide.
Answer to the first question:
When using the Service object for retrieving calendar items from Google Calendar, the token are automatically verified. When they are invalid, they will be refreshed automatically, and stored in the datastore you provided to the flow.
this can also be done manually. A token is valid for 3600 seconds (one hour). When retrieving a token you get this value with the timestamp when it was issued. You could manually determine if a token is valid. If it is not valid call the following async method.
await credents.RefreshtokenAsync(CancellationToken.None);
This function gets you fresh tokens, and stores them in the datastore you provided.
You could check token with tokeninfo and if token is not valid:
- remove credential from datastore
- invoke new auth
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
UserService userService = UserServiceFactory.getUserService();
if (userService.isUserLoggedIn()) {
User user = userService.getCurrentUser();
log.info(String.format("LoggedUser: %s %s", user.getEmail(), user.getUserId()));
Credential credential = this.getCredential();
Tokeninfo tokenInfo = OAuth2Utils.getTokenInfo(credential, null);
if (tokenInfo != null)
log.info(String.format("Token expires in: %d", tokenInfo.getExpiresIn()));
else {
OAuth2Utils.deleteCredential(user.getUserId());
response.sendRedirect(request.getRequestURI()); // recall this servlet to require new user authorization
return;
}
}
public static Tokeninfo getTokenInfo(Credential credential, String accessToken) {
Oauth2 service = new Oauth2.Builder(new NetHttpTransport(), Constant.JSON_FACTORY, credential).setApplicationName(Constant.APP_NAME).build();
Tokeninfo tokenInfo = null;
try {
tokenInfo = service.tokeninfo().setAccessToken( accessToken == null ? credential.getAccessToken() : accessToken ).execute();
} catch (IOException e) {
log.warning("An error occurred: " + e);
}
return tokenInfo;
}
The question is very simple. I'd like to restrict user access with same login from different machines/browsers: only one live user session is possible.
Apache shiro library is used for user authentification and managment.
Of course this could be done using simple synchornized maps and etc. But the question is: Has Apache Shiro special mechanisms for that or not?
Another variant of this question: how to reveice the list of all subjects who are logged in the system using apache shiro?
UPD:
To clarify my question. My desire is to have some code like this (I known, that there isn't such class exception, but the idea must be more clean):
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(login, password);
try {
currentUser.login(token);
} catch (AlreadyAuthenticatedException aae) {
errorMsg = "You should logoff on another machine!";
}
The Shiro sessions are stored in SessionDAO with sessionId as keys. Without extra effort you cannot access a session by a principal (user name). However, you could extend DefaultSecurityManager and check all active sessions by SessionDAO.getActiveSessions.
The following codes could be a simple example (suppose you are not using WebSubject):
public class UniquePrincipalSecurityManager extends org.apache.shiro.mgt.DefaultSecurityManager {
#Override
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
String loginPrincipal = (String) token.getPrincipal();
DefaultSessionManager sm = (DefaultSessionManager) getSessionManager();
for (Session session : sm.getSessionDAO().getActiveSessions()) {
SimplePrincipalCollection p = (SimplePrincipalCollection) session
.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (p != null && loginPrincipal.equals(p.getPrimaryPrincipal())) {
throw new AlreadyAuthenticatedException();
}
}
return super.login(subject, token);
}
}