Skip to content
This repository was archived by the owner on Dec 12, 2018. It is now read-only.

Commit 8c11022

Browse files
author
mrioan
authored
Merge pull request #1214 from stormpath/Issue-1211-mfa-policy-support
1211 Support stormpath_factor_challenge grant type; expose more info …
2 parents c8553cf + c8333b1 commit 8c11022

13 files changed

+631
-18
lines changed

api/src/main/java/com/stormpath/sdk/oauth/Authenticators.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,14 @@ private Authenticators() {
128128
*/
129129
public static final OAuthStormpathSocialRequestAuthenticatorFactory OAUTH_STORMPATH_SOCIAL_GRANT_REQUEST_AUTHENTICATOR =
130130
(OAuthStormpathSocialRequestAuthenticatorFactory) Classes.newInstance("com.stormpath.sdk.impl.oauth.DefaultOAuthStormpathSocialRequestAuthenticatorFactory");
131+
132+
/**
133+
* Constructs {@link OAuthStormpathFactorChallengeGrantRequestAuthenticator}s.
134+
*
135+
* @since 1.3.1
136+
*/
137+
public static final OAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory OAUTH_STORMPATH_FACTOR_CHALLENGE_GRANT_REQUEST_AUTHENTICATOR =
138+
(OAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory) Classes.newInstance("com.stormpath.sdk.impl.oauth.DefaultOAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory");
139+
131140
}
132141

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2016 Stormpath, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.stormpath.sdk.oauth;
17+
18+
/**
19+
* This class represents a request to exchange a multifactor authentication code for a valid OAuth 2.0 access token.
20+
* Using stormpath_factor_challenge grant type
21+
*
22+
* @since 1.3.1
23+
*/
24+
public interface OAuthStormpathFactorChallengeGrantRequestAuthentication extends OAuthGrantRequestAuthentication {
25+
26+
String getChallenge();
27+
28+
String getCode();
29+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2016 Stormpath, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.stormpath.sdk.oauth;
17+
18+
19+
/**
20+
* Interface denoting a Stormpath Factor Challenge Grant-specific {@link OAuthRequestAuthenticator}.
21+
* It is used to authenticate an account using a challenge to a factor and receive in exchange
22+
* a valid OAuth 2.0 token.
23+
*
24+
* @since 1.3.1
25+
*/
26+
public interface OAuthStormpathFactorChallengeGrantRequestAuthenticator extends OAuthRequestAuthenticator<OAuthGrantRequestAuthenticationResult> {
27+
28+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2016 Stormpath, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.stormpath.sdk.oauth;
17+
18+
/**
19+
* A Stormpath Factor Challenge Grant-specific Authenticator Factory.
20+
*
21+
* @since 1.3.1
22+
*/
23+
public interface OAuthStormpathFactorChallengeGrantRequestAuthenticatorFactory extends OAuthRequestAuthenticatorFactory<OAuthStormpathFactorChallengeGrantRequestAuthenticator> {
24+
}

extensions/httpclient/src/test/groovy/com/stormpath/sdk/impl/application/ApplicationIT.groovy

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,18 @@ import com.stormpath.sdk.application.ApplicationAccountStoreMapping
3232
import com.stormpath.sdk.application.ApplicationAccountStoreMappingList
3333
import com.stormpath.sdk.application.Applications
3434
import com.stormpath.sdk.authc.UsernamePasswordRequests
35+
import com.stormpath.sdk.challenge.google.GoogleAuthenticatorChallenge
36+
import com.stormpath.sdk.challenge.sms.SmsChallenge
3537
import com.stormpath.sdk.client.AuthenticationScheme
3638
import com.stormpath.sdk.client.Client
3739
import com.stormpath.sdk.client.ClientIT
3840
import com.stormpath.sdk.directory.AccountStore
3941
import com.stormpath.sdk.directory.Directories
4042
import com.stormpath.sdk.directory.Directory
43+
import com.stormpath.sdk.factor.FactorOptions
44+
import com.stormpath.sdk.factor.Factors
45+
import com.stormpath.sdk.factor.google.GoogleAuthenticatorFactor
46+
import com.stormpath.sdk.factor.sms.SmsFactor
4147
import com.stormpath.sdk.group.Group
4248
import com.stormpath.sdk.group.Groups
4349
import com.stormpath.sdk.http.HttpMethod
@@ -47,6 +53,7 @@ import com.stormpath.sdk.impl.ds.DefaultDataStore
4753
import com.stormpath.sdk.impl.error.DefaultError
4854
import com.stormpath.sdk.impl.http.authc.SAuthc1RequestAuthenticator
4955
import com.stormpath.sdk.impl.idsite.IdSiteClaims
56+
import com.stormpath.sdk.impl.oauth.DefaultOAuthStormpathFactorChallengeGrantRequestAuthentication
5057
import com.stormpath.sdk.impl.resource.AbstractResource
5158
import com.stormpath.sdk.impl.saml.SamlResultStatus
5259
import com.stormpath.sdk.impl.security.ApiKeySecretEncryptionService
@@ -62,6 +69,7 @@ import com.stormpath.sdk.oauth.OAuthPolicy
6269
import com.stormpath.sdk.oauth.OAuthRefreshTokenRequestAuthentication
6370
import com.stormpath.sdk.oauth.OAuthRequestAuthenticator
6471
import com.stormpath.sdk.oauth.OAuthRequests
72+
import com.stormpath.sdk.oauth.OAuthStormpathFactorChallengeGrantRequestAuthentication
6573
import com.stormpath.sdk.oauth.OAuthTokenRevocator
6674
import com.stormpath.sdk.oauth.OAuthTokenRevocators
6775
import com.stormpath.sdk.oauth.RefreshToken
@@ -82,11 +90,19 @@ import io.jsonwebtoken.Jws
8290
import io.jsonwebtoken.JwsHeader
8391
import io.jsonwebtoken.Jwts
8492
import io.jsonwebtoken.SignatureAlgorithm
93+
import org.apache.commons.codec.binary.Base32
8594
import org.apache.commons.codec.binary.Base64
95+
import org.joda.time.DateTime
96+
import org.joda.time.DateTimeZone
8697
import org.testng.annotations.Test
8798

99+
import javax.crypto.Mac
100+
import javax.crypto.spec.SecretKeySpec
88101
import javax.servlet.http.HttpServletRequest
89102
import java.lang.reflect.Field
103+
import java.security.InvalidKeyException
104+
import java.security.NoSuchAlgorithmException
105+
import java.util.concurrent.TimeUnit
90106

91107
import static com.stormpath.sdk.application.Applications.newCreateRequestFor
92108
import static org.easymock.EasyMock.createMock
@@ -1861,6 +1877,143 @@ class ApplicationIT extends ClientIT {
18611877
assertEquals result.getExpiresIn(), 3600
18621878
}
18631879

1880+
/* @since 1.3.1 */
1881+
@Test
1882+
void testCreateStormpathFactorChallengeTokenForGoogleAuthenticatorFactorWithBadCode() {
1883+
def app = createTempApp()
1884+
1885+
def account = createTestAccount(app)
1886+
1887+
GoogleAuthenticatorFactor factor = createGoogleAuthenticatorFactor(account)
1888+
1889+
def challenge = client.instantiate(GoogleAuthenticatorChallenge)
1890+
challenge = factor.createChallenge(challenge)
1891+
1892+
String bogusCode = "000000"
1893+
OAuthStormpathFactorChallengeGrantRequestAuthentication request = new DefaultOAuthStormpathFactorChallengeGrantRequestAuthentication(challenge.href, bogusCode)
1894+
1895+
try {
1896+
Authenticators.OAUTH_STORMPATH_FACTOR_CHALLENGE_GRANT_REQUEST_AUTHENTICATOR.forApplication(app).authenticate(request)
1897+
fail()
1898+
}
1899+
catch (ResourceException re) {
1900+
assertEquals(re.getStatus(), 400)
1901+
assertEquals(re.getCode(), 13104)
1902+
}
1903+
}
1904+
1905+
/* @since 1.3.1 */
1906+
@Test
1907+
void testCreateStormpathFactorChallengeTokenForGoogleAuthenticatorFactorWithValidCode() {
1908+
def app = createTempApp()
1909+
1910+
def account = createTestAccount(app)
1911+
1912+
GoogleAuthenticatorFactor factor = createGoogleAuthenticatorFactor(account)
1913+
1914+
sleepToAvoidCrossingThirtySecondMark()
1915+
1916+
def challenge = client.instantiate(GoogleAuthenticatorChallenge)
1917+
challenge = factor.createChallenge(challenge)
1918+
1919+
String validCode = calculateCurrentTOTP(new Base32().decode(factor.getSecret()))
1920+
1921+
OAuthStormpathFactorChallengeGrantRequestAuthentication request = new DefaultOAuthStormpathFactorChallengeGrantRequestAuthentication(challenge.href, validCode)
1922+
1923+
def result = Authenticators.OAUTH_STORMPATH_FACTOR_CHALLENGE_GRANT_REQUEST_AUTHENTICATOR.forApplication(app).authenticate(request)
1924+
assertNotNull result.getAccessTokenHref()
1925+
assertEquals result.getAccessToken().getHref(), result.getAccessTokenHref()
1926+
assertEquals(result.getAccessToken().getAccount().getHref(), account.getHref())
1927+
assertEquals(result.getAccessToken().getApplication().getHref(), app.getHref())
1928+
assertTrue Strings.hasText(result.getAccessTokenString())
1929+
1930+
assertNotNull result.getRefreshToken().getHref()
1931+
assertEquals(result.getRefreshToken().getAccount().getHref(), account.getHref())
1932+
assertEquals(result.getRefreshToken().getApplication().getHref(), app.getHref())
1933+
1934+
assertEquals result.getTokenType(), "Bearer"
1935+
assertEquals result.getExpiresIn(), 3600
1936+
}
1937+
1938+
private GoogleAuthenticatorFactor createGoogleAuthenticatorFactor(Account account) {
1939+
GoogleAuthenticatorFactor factor = client.instantiate(GoogleAuthenticatorFactor)
1940+
factor = factor.setAccountName("accountName").setIssuer("issuer")
1941+
1942+
def builder = Factors.GOOGLE_AUTHENTICATOR.newCreateRequestFor(factor).createChallenge()
1943+
factor = account.createFactor(builder.build())
1944+
1945+
FactorOptions factorOptions = Factors.options().withMostRecentChallenge()
1946+
factor = client.getResource(factor.href, GoogleAuthenticatorFactor.class, factorOptions)
1947+
return factor
1948+
}
1949+
1950+
private static final String HMAC_HASH_FUNCTION = "HmacSHA1";
1951+
private static final int KEY_MODULUS = (int) Math.pow(10, CODE_DIGITS);
1952+
private static final int CODE_DIGITS = 6;
1953+
1954+
/**
1955+
* Calculates a TOTP from the given key which should agree with the one generated
1956+
* by Google Authenticator when provided with the same key.
1957+
* See https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm
1958+
*
1959+
* @param key the key used to compute the TOTP
1960+
* @return the current TOTP, as would be computed by Google Authenticator
1961+
*/
1962+
private static String calculateCurrentTOTP(byte[] key) {
1963+
long timeCounter = System.currentTimeMillis() / TimeUnit.SECONDS.toMillis(30)
1964+
1965+
byte[] data = new byte[8];
1966+
long value = timeCounter;
1967+
1968+
for (int i = 8; i-- > 0; value >>>= 8) {
1969+
data[i] = (byte) value;
1970+
}
1971+
1972+
SecretKeySpec signKey = new SecretKeySpec(key, HMAC_HASH_FUNCTION);
1973+
1974+
try {
1975+
Mac mac = Mac.getInstance(HMAC_HASH_FUNCTION);
1976+
mac.init(signKey);
1977+
1978+
byte[] hash = mac.doFinal(data);
1979+
1980+
int offset = hash[hash.length - 1] & 0xF;
1981+
1982+
long truncatedHash = 0;
1983+
for (int i = 0; i < 4; ++i) {
1984+
truncatedHash <<= 8;
1985+
1986+
// Java bytes are signed but we need an unsigned integer:
1987+
// cleaning off all but the LSB.
1988+
truncatedHash |= (hash[offset + i] & 0xFF);
1989+
}
1990+
1991+
// Clean bits higher than the 32nd (inclusive) and calculate the
1992+
// module with the maximum validation code value.
1993+
truncatedHash &= 0x7FFFFFFF;
1994+
truncatedHash %= KEY_MODULUS;
1995+
1996+
return String.format("%06d", (int) truncatedHash)
1997+
}
1998+
catch (NoSuchAlgorithmException | InvalidKeyException ex) {
1999+
throw new IllegalStateException(ex);
2000+
}
2001+
}
2002+
2003+
protected void sleepToAvoidCrossingThirtySecondMark() {
2004+
DateTime now = new DateTime(DateTimeZone.UTC)
2005+
int seconds = now.getSecondOfMinute()
2006+
int secondsToWait
2007+
if ((seconds <= 30) && (seconds > 25)) {
2008+
secondsToWait = 31 - seconds
2009+
}
2010+
else if ((seconds <= 60) && (seconds > 55)) {
2011+
secondsToWait = 61 - seconds
2012+
}
2013+
2014+
sleep(secondsToWait * 1000)
2015+
}
2016+
18642017
/* @since 1.0.RC7 */
18652018

18662019
@Test
@@ -2439,4 +2592,5 @@ class ApplicationIT extends ClientIT {
24392592

24402593
assertFalse result.newAccount
24412594
}
2595+
24422596
}

extensions/servlet/src/main/java/com/stormpath/sdk/servlet/filter/oauth/OAuthException.java

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,56 +15,71 @@
1515
*/
1616
package com.stormpath.sdk.servlet.filter.oauth;
1717

18+
import com.fasterxml.jackson.databind.ObjectMapper;
1819
import com.stormpath.sdk.lang.Assert;
1920
import com.stormpath.sdk.lang.Strings;
2021

22+
import java.util.LinkedHashMap;
23+
import java.util.Map;
24+
2125
/**
2226
* @since 1.0.RC3
2327
*/
2428
public class OAuthException extends RuntimeException {
2529

30+
private static final ObjectMapper objectMapper = new ObjectMapper();
31+
2632
private final OAuthErrorCode errorCode;
2733

34+
private Map<String, Object> errorMap;
35+
2836
public OAuthException(OAuthErrorCode code) {
29-
this(code, null, null);
37+
this(code, null, (Exception) null);
3038
}
3139

3240
public OAuthException(OAuthErrorCode code, String message) {
3341
super(message != null ? message : (code != null ? code.getValue() : ""));
3442
Assert.notNull(code, "OAuthErrorCode cannot be null.");
3543
this.errorCode = code;
44+
45+
initializeErrorMap();
3646
}
3747

3848
public OAuthException(OAuthErrorCode code, String message, Exception cause) {
3949
super(message != null ? message : (code != null ? code.getValue() : ""), cause);
4050
Assert.notNull(code, "OAuthErrorCode cannot be null.");
4151
this.errorCode = code;
52+
53+
initializeErrorMap();
4254
}
4355

44-
public OAuthErrorCode getErrorCode() {
45-
return errorCode;
56+
public OAuthException(OAuthErrorCode code, Map<String, Object> error, String message) {
57+
this(code, message, null);
58+
59+
errorMap.putAll(error);
4660
}
4761

48-
public String toJson() {
62+
private void initializeErrorMap() {
63+
errorMap = new LinkedHashMap<>();
4964

50-
String json = "{" + toJson("error", getErrorCode());
65+
errorMap.put("error", errorCode.getValue());
5166

5267
String val = getMessage();
5368
if (Strings.hasText(val)) {
54-
json += "," + toJson("message", val);
69+
errorMap.put("message", val);
5570
}
56-
57-
json += "}";
58-
59-
return json;
6071
}
6172

62-
protected static String toJson(String name, Object value) {
63-
String stringValue = String.valueOf(value);
64-
return quote(name) + ":" + quote(stringValue);
73+
public OAuthErrorCode getErrorCode() {
74+
return errorCode;
6575
}
6676

67-
protected static String quote(String val) {
68-
return "\"" + val + "\"";
77+
public String toJson() {
78+
try {
79+
return objectMapper.writeValueAsString(errorMap);
80+
} catch (Exception e) {
81+
throw new IllegalStateException("Unable to serialize OAuthException to json.", e);
82+
}
6983
}
84+
7085
}

0 commit comments

Comments
 (0)