Java tutorial
/* * Copyright 2013 Stormpath, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.stormpath.spring.security.provider; import com.stormpath.sdk.account.Account; import com.stormpath.sdk.application.Application; import com.stormpath.sdk.authc.AuthenticationRequest; import com.stormpath.sdk.authc.UsernamePasswordRequest; import com.stormpath.sdk.client.Client; import com.stormpath.sdk.group.Group; import com.stormpath.sdk.group.GroupList; import com.stormpath.sdk.resource.ResourceException; import com.stormpath.spring.security.authz.permission.Permission; import com.stormpath.spring.security.util.StringUtils; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; /** * A {@code AuthenticationProvider} implementation that uses the <a href="http://www.stormpath.com">Stormpath</a> Cloud Identity * Management service for authentication and authorization operations for a single Application. * <p/> * The Stormpath-registered * <a href="https://www.stormpath.com/docs/libraries/application-rest-url">Application's Stormpath REST URL</a> * must be configured as the {@code applicationRestUrl} property. * <h3>Authentication</h3> * Once your application's REST URL is configured, this provider implementation automatically executes authentication * attempts without any need of further configuration by interacting with the Application's * <a href="http://www.stormpath.com/docs/rest/api#ApplicationLoginAttempts">loginAttempts endpoint</a>. * <h3>Authorization</h3> * Stormpath Accounts and Groups can be translated to Spring Security granted authorities via the following components. You * can implement implementations of these interfaces and plug them into this provider for custom translation behavior: * <ul> * <li>{@link AccountPermissionResolver AccountPermissionResolver}</li> * <li>{@link GroupPermissionResolver GroupPermissionResolver}</li> * <li>{@link AccountGrantedAuthorityResolver AccountGrantedAuthorityResolver}</li> * <li>{@link GroupGrantedAuthorityResolver GroupGrantedAuthorityResolver}</li> * </ul> * <p/> * This provider implementation comes pre-configured with the following default implementations, which should suit most * Spring Security+Stormpath use cases: * * <table> * <tr> * <th>Property</th> * <th>Pre-configured Implementation</th> * <th>Notes</th> * </tr> * <tr> * <td>{@link #getGroupGrantedAuthorityResolver() groupGrantedAuthorityResolver}</td> * <td>{@link DefaultGroupGrantedAuthorityResolver}</td> * <td>Each Stormpath Group can be represented as up to three possible Spring Security granted authorities (with * 1-to-1 being the default). See the {@link DefaultGroupGrantedAuthorityResolver} JavaDoc for more info.</td> * </tr> * <tr> * <td>{@link #getAccountGrantedAuthorityResolver() accountGrantedAuthorityResolver}</td> * <td>None</td> * <td>Most Spring Security+Stormpath applications should only need the above {@code DefaultGroupGrantedAuthorityResolver} * when using Stormpath Groups as Spring Security granted authorities. This authentication provider implementation * already acquires the {@link com.stormpath.sdk.account.Account#getGroups() account's assigned groups} and resolves the group * granted authorities via the above {@code groupGrantedAuthorityResolver}. <b>You only need to configure this property * if you need an additional way to represent an account's assigned granted authorities that cannot already be * represented via Stormpath account <--> group associations.</td> * </tr> * <tr> * <td>{@link #getGroupPermissionResolver() groupPermissionResolver}</td> * <td>{@link GroupCustomDataPermissionResolver}</td> * <td>The {@code GroupCustomDataPermissionResolver} assumes the convention that a Group's assigned permissions * are stored as a nested {@code Set<String>} field in the * {@link com.stormpath.sdk.group.Group#getCustomData() group's CustomData resource}. See the * {@link GroupCustomDataPermissionResolver} JavaDoc for more information.</td> * </tr> * <tr> * <td>{@link #getAccountPermissionResolver() accountPermissionResolver}</td> * <td>{@link AccountCustomDataPermissionResolver}</td> * <td>The {@code AccountCustomDataPermissionResolver} assumes the convention that an Account's directly * assigned permissions are stored as a nested {@code Set<String>} field in the * {@link com.stormpath.sdk.account.Account#getCustomData() account's CustomData resource}. See the * {@link AccountCustomDataPermissionResolver} JavaDoc for more information.</td> * </tr> * </table> * <h4>Transitive Permissions</h4> * This implementation represents an Account's granted permissions as all permissions that: * <ol> * <li>Are assigned directly to the Account itself</li> * <li>Are assigned to any of the Account's assigned Groups</li> * </ol> * <h4>Assigning Permissions</h4> * A Spring Security Authentication Provider is a read-only component - it typically does not support account/group/permission * updates directly. * Therefore, you make modifications to these components by interacting with the data store (e.g. Stormpath) directly. * <p/> * The {@link com.stormpath.spring.security.authz.CustomDataPermissionsEditor CustomDataPermissionsEditor} has been provided for * this purpose. For example, assuming the convention of storing permissions in an account or group's CustomData * resource: * <pre> * Account account = getAccount(); * new CustomDataPermissionsEditor(account.getCustomData()) * .append("someResourceType:anIdentifier:anAction") * .append("anotherResourceType:anIdentifier:*") * .remove("oldPermission"); * account.save(); * </pre> * Again, the default {@link #getGroupPermissionResolver() groupPermissionResolver} and * {@link #getAccountPermissionResolver() accountPermissionResolver} instances assume this CustomData storage strategy, * so if you use them, the above {@code CustomDataPermissionsEditor} will work easily. * <p/> * When the given credentials are successfully authenticated an {@link AuthenticationTokenFactory AuthenticationTokenFactory} instance * is used to create an authenticated token to be returned to the provider's client. By default, the {@link UsernamePasswordAuthenticationTokenFactory} * is used, constructing {@code UsernamePasswordAuthenticationToken} objects. By default, the principal stored in this token is a {@link StormpathUserDetails} * containing Stormpath account's properties like href, given name, username, etc. It can be easily modified by creating a new * <code>AuthenticationTokenFactory</code> and setting it to this provider via {@link #setAuthenticationTokenFactory(AuthenticationTokenFactory)}. * * @see AccountGrantedAuthorityResolver * @see GroupGrantedAuthorityResolver * @see AuthenticationTokenFactory * @see StormpathUserDetails */ public class StormpathAuthenticationProvider implements AuthenticationProvider { private Client client; private String applicationRestUrl; private GroupGrantedAuthorityResolver groupGrantedAuthorityResolver; private GroupPermissionResolver groupPermissionResolver; private AccountGrantedAuthorityResolver accountGrantedAuthorityResolver; private AccountPermissionResolver accountPermissionResolver; private AuthenticationTokenFactory authenticationTokenFactory; private Application application; //acquired via the client at runtime, not configurable by the StormpathAuthenticationProvider user public StormpathAuthenticationProvider() { setGroupGrantedAuthorityResolver(new DefaultGroupGrantedAuthorityResolver()); setGroupPermissionResolver(new GroupCustomDataPermissionResolver()); setAccountPermissionResolver(new AccountCustomDataPermissionResolver()); setAuthenticationTokenFactory(new UsernamePasswordAuthenticationTokenFactory()); } /** * Returns the {@code Client} instance used to communicate with Stormpath's REST API. * * @return the {@code Client} instance used to communicate with Stormpath's REST API. */ public Client getClient() { return client; } /** * Sets the {@code Client} instance used to communicate with Stormpath's REST API. * * @param client the {@code Client} instance used to communicate with Stormpath's REST API. */ public void setClient(Client client) { this.client = client; } /** * Returns the Stormpath REST URL of the specific application communicating with Stormpath. * <p/> * Any application supported by Stormpath will have a * <a href="http://www.stormpath.com/docs/quickstart/authenticate-account">dedicated unique REST URL</a>. The * Stormpath REST URL of the Spring Security-enabled application communicating with Stormpath via this Provider must be * configured by this property. * * @return the Stormpath REST URL of the specific application communicating with Stormpath. */ public String getApplicationRestUrl() { return applicationRestUrl; } /** * Sets the Stormpath REST URL of the specific application communicating with Stormpath. * <p/> * Any application supported by Stormpath will have a * <a href="http://www.stormpath.com/docs/quickstart/authenticate-account">dedicated unique REST URL</a>. The * Stormpath REST URL of the Spring Security-enabled application communicating with Stormpath via this Provider must be * configured by this property. * * @param applicationRestUrl the Stormpath REST URL of the specific application communicating with Stormpath. */ public void setApplicationRestUrl(String applicationRestUrl) { this.applicationRestUrl = applicationRestUrl; } /** * Returns the {@link GroupGrantedAuthorityResolver} used to translate Stormpath Groups into Spring Security granted authorities. * Unless overridden via {@link #setGroupGrantedAuthorityResolver(GroupGrantedAuthorityResolver) setGroupGrantedAuthorityResolver}, * the default instance is a {@link DefaultGroupGrantedAuthorityResolver}. * * @return the {@link GroupGrantedAuthorityResolver} used to translate Stormpath Groups into Spring Security granted authorities. */ public GroupGrantedAuthorityResolver getGroupGrantedAuthorityResolver() { return groupGrantedAuthorityResolver; } /** * Sets the {@link GroupGrantedAuthorityResolver} used to translate Stormpath Groups into Spring Security granted authorities. * Unless overridden, the default instance is a {@link DefaultGroupGrantedAuthorityResolver}. * * @param groupGrantedAuthorityResolver the {@link GroupGrantedAuthorityResolver} used to translate Stormpath Groups into * Spring Security granted authorities. */ public void setGroupGrantedAuthorityResolver(GroupGrantedAuthorityResolver groupGrantedAuthorityResolver) { this.groupGrantedAuthorityResolver = groupGrantedAuthorityResolver; } /** * Returns the {@link AccountGrantedAuthorityResolver} used to discover a Stormpath Account's assigned permissions. This * is {@code null} by default and must be configured based on your application's needs. * * @return the {@link AccountGrantedAuthorityResolver} used to discover a Stormpath Account's assigned permissions. */ public AccountGrantedAuthorityResolver getAccountGrantedAuthorityResolver() { return accountGrantedAuthorityResolver; } /** * Sets the {@link AccountGrantedAuthorityResolver} used to discover a Stormpath Account's assigned permissions. This * is {@code null} by default and must be configured based on your application's needs. * * @param accountGrantedAuthorityResolver the {@link AccountGrantedAuthorityResolver} used to discover a Stormpath Account's * assigned permissions */ public void setAccountGrantedAuthorityResolver( AccountGrantedAuthorityResolver accountGrantedAuthorityResolver) { this.accountGrantedAuthorityResolver = accountGrantedAuthorityResolver; } /** * Returns the {@link GroupPermissionResolver} used to discover a Stormpath Groups' assigned permissions. Unless * overridden via {@link #setGroupPermissionResolver(GroupPermissionResolver) setGroupPermissionResolver}, the * default instance is a {@link GroupCustomDataPermissionResolver}. * * @return the {@link GroupPermissionResolver} used to discover a Stormpath Groups' assigned permissions * @since 0.2.0 */ public GroupPermissionResolver getGroupPermissionResolver() { return groupPermissionResolver; } /** * Sets the {@link GroupPermissionResolver} used to discover a Stormpath Groups' assigned permissions. Unless * overridden, the default instance is a {@link GroupCustomDataPermissionResolver}. * * @param groupPermissionResolver the {@link GroupPermissionResolver} used to discover a Stormpath Groups' assigned * permissions * @since 0.2.0 */ public void setGroupPermissionResolver(GroupPermissionResolver groupPermissionResolver) { this.groupPermissionResolver = groupPermissionResolver; } /** * Returns the {@link AccountPermissionResolver} used to discover a Stormpath Account's directly-assigned * permissions. Unless overridden via * {@link #setAccountPermissionResolver(AccountPermissionResolver) setAccountPermissionResolver}, the default * instance is a {@link AccountCustomDataPermissionResolver}. * * @return the {@link AccountPermissionResolver} used to discover a Stormpath Account's assigned permissions. * @since 0.2.0 */ public AccountPermissionResolver getAccountPermissionResolver() { return accountPermissionResolver; } /** * Sets the {@link AccountPermissionResolver} used to discover a Stormpath Account's assigned permissions. Unless * overridden, the default instance is a {@link AccountCustomDataPermissionResolver}. * * @param accountPermissionResolver the {@link AccountPermissionResolver} used to discover a Stormpath Account's * assigned permissions * @since 0.2.0 */ public void setAccountPermissionResolver(AccountPermissionResolver accountPermissionResolver) { this.accountPermissionResolver = accountPermissionResolver; } /** * * Returns the {@link AccountGrantedAuthorityResolver} used to discover a Stormpath Account's assigned permissions. Unless * overridden, the default instance is a {@link UsernamePasswordAuthenticationTokenFactory}. * * @return the token factory to be used when creating tokens for the successfully authenticated credentials. */ public AuthenticationTokenFactory getAuthenticationTokenFactory() { return authenticationTokenFactory; } /** * Sets the {@link AuthenticationTokenFactory} used to create authenticated tokens. Unless overridden via * {@link #setAuthenticationTokenFactory(AuthenticationTokenFactory)} setAuthenticationTokenFactory}, * the default instance is a {@link UsernamePasswordAuthenticationTokenFactory}. * * @param authenticationTokenFactory the token factory to be used when creating tokens for the successfully * authenticated credentials. */ public void setAuthenticationTokenFactory(AuthenticationTokenFactory authenticationTokenFactory) { if (authenticationTokenFactory == null) { throw new IllegalArgumentException("authenticationTokenFactory cannot be null."); } this.authenticationTokenFactory = authenticationTokenFactory; } private void assertState() { if (this.client == null) { throw new IllegalStateException("Stormpath SDK Client instance must be configured."); } if (this.applicationRestUrl == null) { throw new IllegalStateException("\n\nThis application's Stormpath REST URL must be configured.\n\n " + "You may get your application's Stormpath REST URL as shown here:\n\n " + "http://www.stormpath.com/docs/application-rest-url\n\n" + "Copy and paste the 'REST URL' value as the 'applicationRestUrl' property of this class."); } } /** * Performs actual authentication for the received authentication credentials using * <a href="http://www.stormpath.com">Stormpath</a> Cloud Identity Management service for a single application. * * @param authentication the authentication request object. * * @return a fully authenticated object including credentials. * * @throws AuthenticationException if authentication fails. */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { assertState(); AuthenticationRequest request = createAuthenticationRequest(authentication); Application application = ensureApplicationReference(); Account account; try { account = application.authenticateAccount(request).getAccount(); } catch (ResourceException e) { String msg = StringUtils.clean(e.getMessage()); if (msg == null) { msg = StringUtils.clean(e.getDeveloperMessage()); } if (msg == null) { msg = "Invalid login or password."; } throw new AuthenticationServiceException(msg, e); } finally { //Clear the request data to prevent later memory access request.clear(); } Authentication authToken = this.authenticationTokenFactory.createAuthenticationToken( authentication.getPrincipal(), null, getGrantedAuthorities(account), account); return authToken; } /** * Returns <code>true</code> if this <Code>AuthenticationProvider</code> supports the indicated * <Code>Authentication</code> object. * * @param authentication the class to validate this <Code>AuthenticationProvider</code> supports * * @return <code>true</code> if the given class is supported */ @Override public boolean supports(Class<?> authentication) { return Authentication.class.isAssignableFrom(authentication); } //This is not thread safe, but the Client is, and this is only executed during initial Application //acquisition, so it is negligible if this executes a few times instead of just once. protected final Application ensureApplicationReference() { if (this.application == null) { String href = getApplicationRestUrl(); this.application = client.getDataStore().getResource(href, Application.class); } return this.application; } protected AuthenticationRequest createAuthenticationRequest(Authentication authentication) { String username = (String) authentication.getPrincipal(); String password = (String) authentication.getCredentials(); return new UsernamePasswordRequest(username, password); } protected Collection<GrantedAuthority> getGrantedAuthorities(Account account) { Collection<GrantedAuthority> grantedAuthorities = new HashSet<GrantedAuthority>(); GroupList groups = account.getGroups(); for (Group group : groups) { Set<GrantedAuthority> groupGrantedAuthorities = resolveGrantedAuthorities(group); grantedAuthorities.addAll(groupGrantedAuthorities); Set<Permission> groupPermissions = resolvePermissions(group); grantedAuthorities.addAll(groupPermissions); } Set<GrantedAuthority> accountGrantedAuthorities = resolveGrantedAuthorities(account); grantedAuthorities.addAll(accountGrantedAuthorities); Set<Permission> accountPermissions = resolvePermissions(account); for (GrantedAuthority permission : accountPermissions) { grantedAuthorities.add(permission); } return grantedAuthorities; } private Set<GrantedAuthority> resolveGrantedAuthorities(Group group) { if (groupGrantedAuthorityResolver != null) { return groupGrantedAuthorityResolver.resolveGrantedAuthorities(group); } return Collections.emptySet(); } private Set<GrantedAuthority> resolveGrantedAuthorities(Account account) { if (accountGrantedAuthorityResolver != null) { return accountGrantedAuthorityResolver.resolveGrantedAuthorities(account); } return Collections.emptySet(); } //since 0.1.1 private Set<Permission> resolvePermissions(Group group) { if (groupPermissionResolver != null) { return groupPermissionResolver.resolvePermissions(group); } return Collections.emptySet(); } //since 0.1.1 private Set<Permission> resolvePermissions(Account account) { if (accountPermissionResolver != null) { return accountPermissionResolver.resolvePermissions(account); } return Collections.emptySet(); } }