diff --git a/api/src/main/java/com/stormpath/sdk/oauth/Authenticators.java b/api/src/main/java/com/stormpath/sdk/oauth/Authenticators.java index 7e4f875ff4..86568b4495 100644 --- a/api/src/main/java/com/stormpath/sdk/oauth/Authenticators.java +++ b/api/src/main/java/com/stormpath/sdk/oauth/Authenticators.java @@ -128,5 +128,14 @@ private Authenticators() { */ public static final OAuthStormpathSocialRequestAuthenticatorFactory OAUTH_STORMPATH_SOCIAL_GRANT_REQUEST_AUTHENTICATOR = (OAuthStormpathSocialRequestAuthenticatorFactory) Classes.newInstance("com.stormpath.sdk.impl.oauth.DefaultOAuthStormpathSocialRequestAuthenticatorFactory"); + + /** + * Constructs {@link OAuthStormpathFactorChallengeGrantRequestAuthenticator}s. + * + * @since 1.3.1 + */ + public static final OAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory OAUTH_STORMPATH_FACTOR_CHALLENGE_GRANT_REQUEST_AUTHENTICATOR = + (OAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory) Classes.newInstance("com.stormpath.sdk.impl.oauth.DefaultOAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory"); + } diff --git a/api/src/main/java/com/stormpath/sdk/oauth/OAuthStormpathFactorChallengeGrantRequestAuthentication.java b/api/src/main/java/com/stormpath/sdk/oauth/OAuthStormpathFactorChallengeGrantRequestAuthentication.java new file mode 100644 index 0000000000..77c3ee79f2 --- /dev/null +++ b/api/src/main/java/com/stormpath/sdk/oauth/OAuthStormpathFactorChallengeGrantRequestAuthentication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 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.sdk.oauth; + +/** + * This class represents a request to exchange a multifactor authentication code for a valid OAuth 2.0 access token. + * Using stormpath_factor_challenge grant type + * + * @since 1.3.1 + */ +public interface OAuthStormpathFactorChallengeGrantRequestAuthentication extends OAuthGrantRequestAuthentication { + + String getChallenge(); + + String getCode(); +} diff --git a/api/src/main/java/com/stormpath/sdk/oauth/OAuthStormpathFactorChallengeGrantRequestAuthenticator.java b/api/src/main/java/com/stormpath/sdk/oauth/OAuthStormpathFactorChallengeGrantRequestAuthenticator.java new file mode 100644 index 0000000000..25f86ac9c8 --- /dev/null +++ b/api/src/main/java/com/stormpath/sdk/oauth/OAuthStormpathFactorChallengeGrantRequestAuthenticator.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016 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.sdk.oauth; + + +/** + * Interface denoting a Stormpath Factor Challenge Grant-specific {@link OAuthRequestAuthenticator}. + * It is used to authenticate an account using a challenge to a factor and receive in exchange + * a valid OAuth 2.0 token. + * + * @since 1.3.1 + */ +public interface OAuthStormpathFactorChallengeGrantRequestAuthenticator extends OAuthRequestAuthenticator { + +} diff --git a/api/src/main/java/com/stormpath/sdk/oauth/OAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory.java b/api/src/main/java/com/stormpath/sdk/oauth/OAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory.java new file mode 100644 index 0000000000..0ffbc7568e --- /dev/null +++ b/api/src/main/java/com/stormpath/sdk/oauth/OAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory.java @@ -0,0 +1,24 @@ +/* + * Copyright 2016 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.sdk.oauth; + +/** + * A Stormpath Factor Challenge Grant-specific Authenticator Factory. + * + * @since 1.3.1 + */ +public interface OAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory extends OAuthRequestAuthenticatorFactory { +} diff --git a/extensions/httpclient/src/test/groovy/com/stormpath/sdk/impl/application/ApplicationIT.groovy b/extensions/httpclient/src/test/groovy/com/stormpath/sdk/impl/application/ApplicationIT.groovy index e3eb75c703..20a11ba962 100644 --- a/extensions/httpclient/src/test/groovy/com/stormpath/sdk/impl/application/ApplicationIT.groovy +++ b/extensions/httpclient/src/test/groovy/com/stormpath/sdk/impl/application/ApplicationIT.groovy @@ -32,12 +32,18 @@ import com.stormpath.sdk.application.ApplicationAccountStoreMapping import com.stormpath.sdk.application.ApplicationAccountStoreMappingList import com.stormpath.sdk.application.Applications import com.stormpath.sdk.authc.UsernamePasswordRequests +import com.stormpath.sdk.challenge.google.GoogleAuthenticatorChallenge +import com.stormpath.sdk.challenge.sms.SmsChallenge import com.stormpath.sdk.client.AuthenticationScheme import com.stormpath.sdk.client.Client import com.stormpath.sdk.client.ClientIT import com.stormpath.sdk.directory.AccountStore import com.stormpath.sdk.directory.Directories import com.stormpath.sdk.directory.Directory +import com.stormpath.sdk.factor.FactorOptions +import com.stormpath.sdk.factor.Factors +import com.stormpath.sdk.factor.google.GoogleAuthenticatorFactor +import com.stormpath.sdk.factor.sms.SmsFactor import com.stormpath.sdk.group.Group import com.stormpath.sdk.group.Groups import com.stormpath.sdk.http.HttpMethod @@ -47,6 +53,7 @@ import com.stormpath.sdk.impl.ds.DefaultDataStore import com.stormpath.sdk.impl.error.DefaultError import com.stormpath.sdk.impl.http.authc.SAuthc1RequestAuthenticator import com.stormpath.sdk.impl.idsite.IdSiteClaims +import com.stormpath.sdk.impl.oauth.DefaultOAuthStormpathFactorChallengeGrantRequestAuthentication import com.stormpath.sdk.impl.resource.AbstractResource import com.stormpath.sdk.impl.saml.SamlResultStatus import com.stormpath.sdk.impl.security.ApiKeySecretEncryptionService @@ -62,6 +69,7 @@ import com.stormpath.sdk.oauth.OAuthPolicy import com.stormpath.sdk.oauth.OAuthRefreshTokenRequestAuthentication import com.stormpath.sdk.oauth.OAuthRequestAuthenticator import com.stormpath.sdk.oauth.OAuthRequests +import com.stormpath.sdk.oauth.OAuthStormpathFactorChallengeGrantRequestAuthentication import com.stormpath.sdk.oauth.OAuthTokenRevocator import com.stormpath.sdk.oauth.OAuthTokenRevocators import com.stormpath.sdk.oauth.RefreshToken @@ -82,11 +90,19 @@ import io.jsonwebtoken.Jws import io.jsonwebtoken.JwsHeader import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm +import org.apache.commons.codec.binary.Base32 import org.apache.commons.codec.binary.Base64 +import org.joda.time.DateTime +import org.joda.time.DateTimeZone import org.testng.annotations.Test +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec import javax.servlet.http.HttpServletRequest import java.lang.reflect.Field +import java.security.InvalidKeyException +import java.security.NoSuchAlgorithmException +import java.util.concurrent.TimeUnit import static com.stormpath.sdk.application.Applications.newCreateRequestFor import static org.easymock.EasyMock.createMock @@ -1861,6 +1877,143 @@ class ApplicationIT extends ClientIT { assertEquals result.getExpiresIn(), 3600 } + /* @since 1.3.1 */ + @Test + void testCreateStormpathFactorChallengeTokenForGoogleAuthenticatorFactorWithBadCode() { + def app = createTempApp() + + def account = createTestAccount(app) + + GoogleAuthenticatorFactor factor = createGoogleAuthenticatorFactor(account) + + def challenge = client.instantiate(GoogleAuthenticatorChallenge) + challenge = factor.createChallenge(challenge) + + String bogusCode = "000000" + OAuthStormpathFactorChallengeGrantRequestAuthentication request = new DefaultOAuthStormpathFactorChallengeGrantRequestAuthentication(challenge.href, bogusCode) + + try { + Authenticators.OAUTH_STORMPATH_FACTOR_CHALLENGE_GRANT_REQUEST_AUTHENTICATOR.forApplication(app).authenticate(request) + fail() + } + catch (ResourceException re) { + assertEquals(re.getStatus(), 400) + assertEquals(re.getCode(), 13104) + } + } + + /* @since 1.3.1 */ + @Test + void testCreateStormpathFactorChallengeTokenForGoogleAuthenticatorFactorWithValidCode() { + def app = createTempApp() + + def account = createTestAccount(app) + + GoogleAuthenticatorFactor factor = createGoogleAuthenticatorFactor(account) + + sleepToAvoidCrossingThirtySecondMark() + + def challenge = client.instantiate(GoogleAuthenticatorChallenge) + challenge = factor.createChallenge(challenge) + + String validCode = calculateCurrentTOTP(new Base32().decode(factor.getSecret())) + + OAuthStormpathFactorChallengeGrantRequestAuthentication request = new DefaultOAuthStormpathFactorChallengeGrantRequestAuthentication(challenge.href, validCode) + + def result = Authenticators.OAUTH_STORMPATH_FACTOR_CHALLENGE_GRANT_REQUEST_AUTHENTICATOR.forApplication(app).authenticate(request) + assertNotNull result.getAccessTokenHref() + assertEquals result.getAccessToken().getHref(), result.getAccessTokenHref() + assertEquals(result.getAccessToken().getAccount().getHref(), account.getHref()) + assertEquals(result.getAccessToken().getApplication().getHref(), app.getHref()) + assertTrue Strings.hasText(result.getAccessTokenString()) + + assertNotNull result.getRefreshToken().getHref() + assertEquals(result.getRefreshToken().getAccount().getHref(), account.getHref()) + assertEquals(result.getRefreshToken().getApplication().getHref(), app.getHref()) + + assertEquals result.getTokenType(), "Bearer" + assertEquals result.getExpiresIn(), 3600 + } + + private GoogleAuthenticatorFactor createGoogleAuthenticatorFactor(Account account) { + GoogleAuthenticatorFactor factor = client.instantiate(GoogleAuthenticatorFactor) + factor = factor.setAccountName("accountName").setIssuer("issuer") + + def builder = Factors.GOOGLE_AUTHENTICATOR.newCreateRequestFor(factor).createChallenge() + factor = account.createFactor(builder.build()) + + FactorOptions factorOptions = Factors.options().withMostRecentChallenge() + factor = client.getResource(factor.href, GoogleAuthenticatorFactor.class, factorOptions) + return factor + } + + private static final String HMAC_HASH_FUNCTION = "HmacSHA1"; + private static final int KEY_MODULUS = (int) Math.pow(10, CODE_DIGITS); + private static final int CODE_DIGITS = 6; + + /** + * Calculates a TOTP from the given key which should agree with the one generated + * by Google Authenticator when provided with the same key. + * See https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm + * + * @param key the key used to compute the TOTP + * @return the current TOTP, as would be computed by Google Authenticator + */ + private static String calculateCurrentTOTP(byte[] key) { + long timeCounter = System.currentTimeMillis() / TimeUnit.SECONDS.toMillis(30) + + byte[] data = new byte[8]; + long value = timeCounter; + + for (int i = 8; i-- > 0; value >>>= 8) { + data[i] = (byte) value; + } + + SecretKeySpec signKey = new SecretKeySpec(key, HMAC_HASH_FUNCTION); + + try { + Mac mac = Mac.getInstance(HMAC_HASH_FUNCTION); + mac.init(signKey); + + byte[] hash = mac.doFinal(data); + + int offset = hash[hash.length - 1] & 0xF; + + long truncatedHash = 0; + for (int i = 0; i < 4; ++i) { + truncatedHash <<= 8; + + // Java bytes are signed but we need an unsigned integer: + // cleaning off all but the LSB. + truncatedHash |= (hash[offset + i] & 0xFF); + } + + // Clean bits higher than the 32nd (inclusive) and calculate the + // module with the maximum validation code value. + truncatedHash &= 0x7FFFFFFF; + truncatedHash %= KEY_MODULUS; + + return String.format("%06d", (int) truncatedHash) + } + catch (NoSuchAlgorithmException | InvalidKeyException ex) { + throw new IllegalStateException(ex); + } + } + + protected void sleepToAvoidCrossingThirtySecondMark() { + DateTime now = new DateTime(DateTimeZone.UTC) + int seconds = now.getSecondOfMinute() + int secondsToWait + if ((seconds <= 30) && (seconds > 25)) { + secondsToWait = 31 - seconds + } + else if ((seconds <= 60) && (seconds > 55)) { + secondsToWait = 61 - seconds + } + + sleep(secondsToWait * 1000) + } + /* @since 1.0.RC7 */ @Test @@ -2439,4 +2592,5 @@ class ApplicationIT extends ClientIT { assertFalse result.newAccount } + } diff --git a/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/filter/oauth/OAuthException.java b/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/filter/oauth/OAuthException.java index 38864c5be4..e84874db04 100644 --- a/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/filter/oauth/OAuthException.java +++ b/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/filter/oauth/OAuthException.java @@ -15,56 +15,71 @@ */ package com.stormpath.sdk.servlet.filter.oauth; +import com.fasterxml.jackson.databind.ObjectMapper; import com.stormpath.sdk.lang.Assert; import com.stormpath.sdk.lang.Strings; +import java.util.LinkedHashMap; +import java.util.Map; + /** * @since 1.0.RC3 */ public class OAuthException extends RuntimeException { + private static final ObjectMapper objectMapper = new ObjectMapper(); + private final OAuthErrorCode errorCode; + private Map errorMap; + public OAuthException(OAuthErrorCode code) { - this(code, null, null); + this(code, null, (Exception) null); } public OAuthException(OAuthErrorCode code, String message) { super(message != null ? message : (code != null ? code.getValue() : "")); Assert.notNull(code, "OAuthErrorCode cannot be null."); this.errorCode = code; + + initializeErrorMap(); } public OAuthException(OAuthErrorCode code, String message, Exception cause) { super(message != null ? message : (code != null ? code.getValue() : ""), cause); Assert.notNull(code, "OAuthErrorCode cannot be null."); this.errorCode = code; + + initializeErrorMap(); } - public OAuthErrorCode getErrorCode() { - return errorCode; + public OAuthException(OAuthErrorCode code, Map error, String message) { + this(code, message, null); + + errorMap.putAll(error); } - public String toJson() { + private void initializeErrorMap() { + errorMap = new LinkedHashMap<>(); - String json = "{" + toJson("error", getErrorCode()); + errorMap.put("error", errorCode.getValue()); String val = getMessage(); if (Strings.hasText(val)) { - json += "," + toJson("message", val); + errorMap.put("message", val); } - - json += "}"; - - return json; } - protected static String toJson(String name, Object value) { - String stringValue = String.valueOf(value); - return quote(name) + ":" + quote(stringValue); + public OAuthErrorCode getErrorCode() { + return errorCode; } - protected static String quote(String val) { - return "\"" + val + "\""; + public String toJson() { + try { + return objectMapper.writeValueAsString(errorMap); + } catch (Exception e) { + throw new IllegalStateException("Unable to serialize OAuthException to json.", e); + } } + } diff --git a/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/mvc/AccessTokenController.java b/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/mvc/AccessTokenController.java index ce08b2c35b..c550c7472a 100644 --- a/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/mvc/AccessTokenController.java +++ b/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/mvc/AccessTokenController.java @@ -23,6 +23,7 @@ import com.stormpath.sdk.impl.authc.DefaultHttpServletRequestWrapper; import com.stormpath.sdk.impl.error.DefaultError; import com.stormpath.sdk.impl.oauth.DefaultIdSiteAuthenticationRequest; +import com.stormpath.sdk.impl.oauth.DefaultOAuthStormpathFactorChallengeGrantRequestAuthentication; import com.stormpath.sdk.impl.oauth.DefaultOAuthStormpathSocialGrantRequestAuthentication; import com.stormpath.sdk.lang.Assert; import com.stormpath.sdk.oauth.AccessTokenResult; @@ -33,6 +34,7 @@ import com.stormpath.sdk.oauth.OAuthPasswordGrantRequestAuthentication; import com.stormpath.sdk.oauth.OAuthRefreshTokenRequestAuthentication; import com.stormpath.sdk.oauth.OAuthRequests; +import com.stormpath.sdk.oauth.OAuthStormpathFactorChallengeGrantRequestAuthentication; import com.stormpath.sdk.oauth.OAuthStormpathSocialGrantRequestAuthentication; import com.stormpath.sdk.resource.ResourceException; import com.stormpath.sdk.servlet.authc.FailedAuthenticationRequestEvent; @@ -58,6 +60,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.util.LinkedHashMap; +import java.util.Map; /** * @since 1.0.RC4 @@ -71,8 +75,18 @@ public class AccessTokenController extends AbstractController { private static final String STORMPATH_SOCIAL_GRANT_TYPE = "stormpath_social"; private static final String STORMPATH_TOKEN_GRANT_TYPE = "stormpath_token"; private static final String REFRESH_TOKEN_GRANT_TYPE = "refresh_token"; + private static final String STORMPATH_FACTOR_CHALLENGE_GRANT_TYPE = "stormpath_factor_challenge"; private static final String GRANT_TYPE_PARAM_NAME = "grant_type"; + private static final String OAUTH_RESPONSE_ERROR = "error"; + private static final String OAUTH_RESPONSE_ACTION = "action"; + private static final String OAUTH_RESPONSE_ERROR_DESCRIPTION = "error_description"; + private static final String OAUTH_RESPONSE_STATE = "state"; + private static final String OAUTH_RESPONSE_ALLOWED_FACTOR_TYPES = "allowedFactorTypes"; + private static final String OAUTH_RESPONSE_FACTOR = "factor"; + private static final String OAUTH_RESPONSE_CHALLENGE = "challenge"; + private static final String OAUTH_RESPONSE_FACTORS = "factors"; + private RefreshTokenResultFactory refreshTokenResultFactory; private RefreshTokenAuthenticationRequestFactory refreshTokenAuthenticationRequestFactory; private RequestAuthorizer requestAuthorizer; @@ -297,19 +311,81 @@ protected AccessTokenResult stormpathSocialAuthenticationRequest(HttpServletRequ return createAccessTokenResult(request, response, authenticationResult); } - private OAuthException convertToOAuthException(ResourceException e, OAuthErrorCode defaultErrorCode) { - com.stormpath.sdk.error.Error error = e.getStormpathError(); + /** + * @since 1.3.1 + */ + protected AccessTokenResult stormpathFactorChallengeAuthenticationRequest(HttpServletRequest request, HttpServletResponse response) { + OAuthGrantRequestAuthenticationResult authenticationResult; + + try { + Application app = getApplication(request); + String challenge = request.getParameter("challenge"); + String code = request.getParameter("code"); + OAuthStormpathFactorChallengeGrantRequestAuthentication grantRequestAuthentication = + new DefaultOAuthStormpathFactorChallengeGrantRequestAuthentication(challenge, code); + + authenticationResult = Authenticators.OAUTH_STORMPATH_FACTOR_CHALLENGE_GRANT_REQUEST_AUTHENTICATOR + .forApplication(app) + .authenticate(grantRequestAuthentication); + } catch (ResourceException e) { + log.debug("Unable to authenticate stormpath social grant request: {}", e.getMessage(), e); + throw convertToOAuthException(e, OAuthErrorCode.INVALID_CLIENT); + } catch (IllegalArgumentException ex) { + throw new OAuthException(OAuthErrorCode.INVALID_REQUEST); + } + + return createAccessTokenResult(request, response, authenticationResult); + } + + /** + * Takes a {@link ResourceException ResourceException} thrown when retrieving an oauth token and exposes + * select fields from its Error to be returned as an {@link OAuthException OAuthException}. + * + * @param resourceException the ResourceException to convert + * @param defaultErrorCode the OAuthErrorCode to use in case none is supplied in the underlying Error + * @return an OAuthException exposing select fields depending on the value of the action property + */ + private OAuthException convertToOAuthException(ResourceException resourceException, OAuthErrorCode defaultErrorCode) { + com.stormpath.sdk.error.Error error = resourceException.getStormpathError(); String message = error.getMessage(); OAuthErrorCode oauthError = defaultErrorCode; if (error instanceof DefaultError) { - Object errorObject = ((DefaultError) error).getProperty("error"); + DefaultError defaultError = ((DefaultError) error); + + Object errorObject = defaultError.getProperty(OAUTH_RESPONSE_ERROR); oauthError = errorObject == null ? oauthError : new OAuthErrorCode(errorObject.toString()); + + Object action = defaultError.getProperty(OAUTH_RESPONSE_ACTION); + if (action instanceof String) { + // get action map from error based on the action + Map errorMap = new LinkedHashMap<>(); + exposeOAuthErrorProperty(errorMap, defaultError, OAUTH_RESPONSE_ERROR_DESCRIPTION); + exposeOAuthErrorProperty(errorMap, defaultError, OAUTH_RESPONSE_ACTION); + if ("factor_enroll".equals(action)) { + exposeOAuthErrorProperty(errorMap, defaultError, OAUTH_RESPONSE_STATE); + exposeOAuthErrorProperty(errorMap, defaultError, OAUTH_RESPONSE_ALLOWED_FACTOR_TYPES); + } + else if ("factor_challenge".equals(action)) { + exposeOAuthErrorProperty(errorMap, defaultError, OAUTH_RESPONSE_STATE); + exposeOAuthErrorProperty(errorMap, defaultError, OAUTH_RESPONSE_ALLOWED_FACTOR_TYPES); + exposeOAuthErrorProperty(errorMap, defaultError, OAUTH_RESPONSE_FACTOR); + exposeOAuthErrorProperty(errorMap, defaultError, OAUTH_RESPONSE_CHALLENGE); + } + else if ("factor_select".equals(action)) { + exposeOAuthErrorProperty(errorMap, defaultError, OAUTH_RESPONSE_FACTORS); + } + return new OAuthException(oauthError, errorMap, ""); + } } return new OAuthException(oauthError, message); } + private void exposeOAuthErrorProperty(Map errorMap, DefaultError defaultError, String propertyName) { + errorMap.put(propertyName, defaultError.getProperty(propertyName)); + } + protected AccessTokenResult stormpathTokenAuthenticationRequest(HttpServletRequest request, HttpServletResponse response) { OAuthGrantRequestAuthenticationResult authenticationResult; @@ -439,6 +515,14 @@ protected AccessTokenResult getAccessTokenResult(String grantType, HttpServletRe throw new OAuthException(OAuthErrorCode.INVALID_CLIENT); } break; + case STORMPATH_FACTOR_CHALLENGE_GRANT_TYPE: + try { + result = this.stormpathFactorChallengeAuthenticationRequest(request, response); + } catch (HttpAuthenticationException e) { + log.warn("Unable to authenticate client", e); + throw new OAuthException(OAuthErrorCode.INVALID_CLIENT); + } + break; default: throw new OAuthException(OAuthErrorCode.UNSUPPORTED_GRANT_TYPE, "'" + grantType + "' is an unsupported grant type."); } diff --git a/impl/src/main/java/com/stormpath/sdk/impl/application/DefaultApplication.java b/impl/src/main/java/com/stormpath/sdk/impl/application/DefaultApplication.java index 4bf11a282a..6286f87758 100644 --- a/impl/src/main/java/com/stormpath/sdk/impl/application/DefaultApplication.java +++ b/impl/src/main/java/com/stormpath/sdk/impl/application/DefaultApplication.java @@ -62,6 +62,7 @@ import com.stormpath.sdk.impl.oauth.DefaultOAuthClientCredentialsGrantRequestAuthenticator; import com.stormpath.sdk.impl.oauth.DefaultOAuthPasswordGrantRequestAuthenticator; import com.stormpath.sdk.impl.oauth.DefaultOAuthRefreshTokenRequestAuthenticator; +import com.stormpath.sdk.impl.oauth.DefaultOAuthStormpathFactorChallengeGrantRequestAuthenticator; import com.stormpath.sdk.impl.oauth.DefaultOAuthStormpathSocialGrantRequestAuthenticator; import com.stormpath.sdk.impl.oauth.DefaultOAuthTokenRevocator; import com.stormpath.sdk.impl.provider.ProviderAccountResolver; @@ -86,6 +87,7 @@ import com.stormpath.sdk.oauth.OAuthPasswordGrantRequestAuthenticator; import com.stormpath.sdk.oauth.OAuthPolicy; import com.stormpath.sdk.oauth.OAuthRefreshTokenRequestAuthenticator; +import com.stormpath.sdk.oauth.OAuthStormpathFactorChallengeGrantRequestAuthenticator; import com.stormpath.sdk.oauth.OAuthStormpathSocialGrantRequestAuthenticator; import com.stormpath.sdk.oauth.OAuthTokenRevocator; import com.stormpath.sdk.organization.Organization; @@ -881,6 +883,11 @@ public OAuthStormpathSocialGrantRequestAuthenticator createStormpathSocialGrantA return new DefaultOAuthStormpathSocialGrantRequestAuthenticator(this, getDataStore()); } + /* @since 1.3.1 */ + public OAuthStormpathFactorChallengeGrantRequestAuthenticator createStormpathFactorChallengeGrantAuthenticator() { + return new DefaultOAuthStormpathFactorChallengeGrantRequestAuthenticator(this, getDataStore()); + } + /* @since 1.0.RC7 */ public OAuthPasswordGrantRequestAuthenticator createPasswordGrantAuthenticator() { return new DefaultOAuthPasswordGrantRequestAuthenticator(this, getDataStore()); diff --git a/impl/src/main/java/com/stormpath/sdk/impl/oauth/DefaultOAuthStormpathFactorChallengeGrantAuthenticationAttempt.java b/impl/src/main/java/com/stormpath/sdk/impl/oauth/DefaultOAuthStormpathFactorChallengeGrantAuthenticationAttempt.java new file mode 100644 index 0000000000..29ab6d3dbd --- /dev/null +++ b/impl/src/main/java/com/stormpath/sdk/impl/oauth/DefaultOAuthStormpathFactorChallengeGrantAuthenticationAttempt.java @@ -0,0 +1,75 @@ +/* + * Copyright 2016 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.sdk.impl.oauth; + +import com.stormpath.sdk.impl.ds.InternalDataStore; +import com.stormpath.sdk.impl.resource.AbstractResource; +import com.stormpath.sdk.impl.resource.Property; +import com.stormpath.sdk.impl.resource.StringProperty; + +import java.util.Map; + +/** + * @since 1.3.1 + */ +public class DefaultOAuthStormpathFactorChallengeGrantAuthenticationAttempt extends AbstractResource implements OAuthStormpathFactorChallengeGrantAuthenticationAttempt { + + static final StringProperty GRANT_TYPE = new StringProperty("grant_type"); + static final StringProperty CHALLENGE = new StringProperty("challenge"); + static final StringProperty CODE = new StringProperty("code"); + + private static final Map PROPERTY_DESCRIPTORS = createPropertyDescriptorMap(GRANT_TYPE, CHALLENGE, CODE); + + public DefaultOAuthStormpathFactorChallengeGrantAuthenticationAttempt(InternalDataStore dataStore) { + super(dataStore); + } + + public DefaultOAuthStormpathFactorChallengeGrantAuthenticationAttempt(InternalDataStore dataStore, Map properties) { + super(dataStore, properties); + } + + @Override + public void setGrantType(String grantType) { + setProperty(GRANT_TYPE, grantType); + } + + @Override + public void setChallenge(String challenge) { + setProperty(CHALLENGE, challenge); + } + + @Override + public void setCode(String code) { + setProperty(CODE, code); + } + + public String getGrantType() { + return getString(GRANT_TYPE); + } + + public String getChallenge() { + return getString(CHALLENGE); + } + + public String getCode() { + return getString(CODE); + } + + @Override + public Map getPropertyDescriptors() { + return PROPERTY_DESCRIPTORS; + } +} diff --git a/impl/src/main/java/com/stormpath/sdk/impl/oauth/DefaultOAuthStormpathFactorChallengeGrantRequestAuthentication.java b/impl/src/main/java/com/stormpath/sdk/impl/oauth/DefaultOAuthStormpathFactorChallengeGrantRequestAuthentication.java new file mode 100644 index 0000000000..39e9c59313 --- /dev/null +++ b/impl/src/main/java/com/stormpath/sdk/impl/oauth/DefaultOAuthStormpathFactorChallengeGrantRequestAuthentication.java @@ -0,0 +1,53 @@ +/* + * Copyright 2016 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.sdk.impl.oauth; + +import com.stormpath.sdk.lang.Assert; +import com.stormpath.sdk.oauth.OAuthStormpathFactorChallengeGrantRequestAuthentication; +import com.stormpath.sdk.oauth.OAuthStormpathSocialGrantRequestAuthentication; + +/** + * @since 1.3.1 + */ +public class DefaultOAuthStormpathFactorChallengeGrantRequestAuthentication implements OAuthStormpathFactorChallengeGrantRequestAuthentication { + private final static String grant_type = "stormpath_factor_challenge"; + + private String challenge; + private String code; + + public DefaultOAuthStormpathFactorChallengeGrantRequestAuthentication(String challenge, String code) { + Assert.hasText(challenge, "challenge cannot be null or empty."); + Assert.hasText(code, "code cannot be null or empty."); + + this.challenge = challenge; + this.code = code; + } + + @Override + public String getChallenge() { + return challenge; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getGrantType() { + return grant_type; + } +} diff --git a/impl/src/main/java/com/stormpath/sdk/impl/oauth/DefaultOAuthStormpathFactorChallengeGrantRequestAuthenticator.java b/impl/src/main/java/com/stormpath/sdk/impl/oauth/DefaultOAuthStormpathFactorChallengeGrantRequestAuthenticator.java new file mode 100644 index 0000000000..e5ba4766b1 --- /dev/null +++ b/impl/src/main/java/com/stormpath/sdk/impl/oauth/DefaultOAuthStormpathFactorChallengeGrantRequestAuthenticator.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016 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.sdk.impl.oauth; + +import com.stormpath.sdk.application.Application; +import com.stormpath.sdk.ds.DataStore; +import com.stormpath.sdk.impl.http.HttpHeaders; +import com.stormpath.sdk.impl.http.MediaType; +import com.stormpath.sdk.lang.Assert; +import com.stormpath.sdk.oauth.GrantAuthenticationToken; +import com.stormpath.sdk.oauth.OAuthGrantRequestAuthenticationResult; +import com.stormpath.sdk.oauth.OAuthRequestAuthentication; +import com.stormpath.sdk.oauth.OAuthStormpathFactorChallengeGrantRequestAuthentication; +import com.stormpath.sdk.oauth.OAuthStormpathFactorChallengeGrantRequestAuthenticator; + +/** + * @since 1.3.1 + */ +public class DefaultOAuthStormpathFactorChallengeGrantRequestAuthenticator extends AbstractOAuthRequestAuthenticator implements OAuthStormpathFactorChallengeGrantRequestAuthenticator { + + private final static String OAUTH_TOKEN_PATH = "/oauth/token"; + + public DefaultOAuthStormpathFactorChallengeGrantRequestAuthenticator(Application application, DataStore dataStore) { + super(application, dataStore); + } + + @Override + public OAuthGrantRequestAuthenticationResult authenticate(OAuthRequestAuthentication authenticationRequest) { + Assert.notNull(this.application, "application cannot be null or empty"); + Assert.isInstanceOf(OAuthStormpathFactorChallengeGrantRequestAuthentication.class, authenticationRequest, "authenticationRequest must be an instance of OAuthStormpathFactorChallengeGrantRequestAuthentication."); + OAuthStormpathFactorChallengeGrantRequestAuthentication authentication = (OAuthStormpathFactorChallengeGrantRequestAuthentication) authenticationRequest; + + OAuthStormpathFactorChallengeGrantAuthenticationAttempt authenticationAttempt = new DefaultOAuthStormpathFactorChallengeGrantAuthenticationAttempt(dataStore); + authenticationAttempt.setGrantType(authentication.getGrantType()); + authenticationAttempt.setChallenge(authentication.getChallenge()); + authenticationAttempt.setCode(authentication.getCode()); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + GrantAuthenticationToken grantResult = dataStore.create(application.getHref() + OAUTH_TOKEN_PATH, authenticationAttempt, GrantAuthenticationToken.class, httpHeaders); + + OAuthGrantRequestAuthenticationResultBuilder builder = new DefaultOAuthGrantRequestAuthenticationResultBuilder(grantResult); + + return builder.build(); + } +} diff --git a/impl/src/main/java/com/stormpath/sdk/impl/oauth/DefaultOAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory.java b/impl/src/main/java/com/stormpath/sdk/impl/oauth/DefaultOAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory.java new file mode 100644 index 0000000000..890412929b --- /dev/null +++ b/impl/src/main/java/com/stormpath/sdk/impl/oauth/DefaultOAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright 2016 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.sdk.impl.oauth; + +import com.stormpath.sdk.application.Application; +import com.stormpath.sdk.impl.application.DefaultApplication; +import com.stormpath.sdk.oauth.OAuthStormpathFactorChallengeGrantRequestAuthenticator; +import com.stormpath.sdk.oauth.OAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory; + +/** + * @since 1.3.1 + */ +public class DefaultOAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory implements OAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory { + @Override + public OAuthStormpathFactorChallengeGrantRequestAuthenticator forApplication(Application application) { + return ((DefaultApplication) application).createStormpathFactorChallengeGrantAuthenticator(); + } +} diff --git a/impl/src/main/java/com/stormpath/sdk/impl/oauth/OAuthStormpathFactorChallengeGrantAuthenticationAttempt.java b/impl/src/main/java/com/stormpath/sdk/impl/oauth/OAuthStormpathFactorChallengeGrantAuthenticationAttempt.java new file mode 100644 index 0000000000..4487bd2c42 --- /dev/null +++ b/impl/src/main/java/com/stormpath/sdk/impl/oauth/OAuthStormpathFactorChallengeGrantAuthenticationAttempt.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016 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.sdk.impl.oauth; + +import com.stormpath.sdk.resource.Resource; + +/** + * This class is used to wrap the information required to request an OAuth token in exchange for verifying + * a challenge to an authentication factor. + * + * @since 1.3.1 + */ +public interface OAuthStormpathFactorChallengeGrantAuthenticationAttempt extends Resource { + /** + * Method used to set the Authentication Grant Type that will be used for the token exchange request. + * @param grantType the Authentication Grant Type that will be used for the token exchange request. + */ + void setGrantType(String grantType); + + /** + * Method used to set the href of the challenge to be verified. + * @param challenge the href of the challenge to be verified. + */ + void setChallenge(String challenge); + + /** + * Method used to set the code for the multifactor challenge. + * @param code the code for the multifactor challenge. + */ + void setCode(String code); +}