Skip to content
This repository has been archived by the owner on Jun 16, 2021. It is now read-only.

Merged okta tck fixes #332

Merged
merged 7 commits into from
Apr 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#Stormpath is Joining Okta
# Stormpath is Joining Okta
We are incredibly excited to announce that [Stormpath is joining forces with Okta](https://stormpath.com/blog/stormpaths-new-path?utm_source=github&utm_medium=readme&utm-campaign=okta-announcement). Please visit [the Migration FAQs](https://stormpath.com/oktaplusstormpath?utm_source=github&utm_medium=readme&utm-campaign=okta-announcement) for a detailed look at what this means for Stormpath users.

We're available to answer all questions at [[email protected]](mailto:[email protected]).
Expand Down Expand Up @@ -49,6 +49,12 @@ Once your web app is running, you can run the TCK against this webapp:
FACEBOOK_CLIENT_SECRET=<Facebook secret for login tests> \
mvn clean -Prun-ITs verify

**NOTE:** If you are running against in Okta application you will need to include the following environment variables:

STORMPATH_TCK_VALIDATE_JWT_URL=https://dev-123456.oktapreview.com/oauth2/<as_id>/v1/keys
STORMPATH_TCK_EMAIL_DOMAIN=<your from email domain>


This will run all tests against the targeted webapp.

NOTE: The 3 environment variables shown above are *required* in order to run the TCK.
Expand Down
2 changes: 2 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
<stormpath.tck.webapp.host>localhost</stormpath.tck.webapp.host>
<stormpath.tck.webapp.port>8080</stormpath.tck.webapp.port>
<failsafe.groups>v100,json,html</failsafe.groups>
<failsafe.excludedGroups>stormpath_only</failsafe.excludedGroups>

</properties>

Expand Down Expand Up @@ -197,6 +198,7 @@
<stormpath.tck.webapp.port>${stormpath.tck.webapp.port}</stormpath.tck.webapp.port>
</systemProperties>
<groups>${failsafe.groups}</groups>
<excludedGroups>${failsafe.excludedGroups}</excludedGroups>
</configuration>
<executions>
<execution>
Expand Down
1 change: 1 addition & 0 deletions src/main/groovy/com/stormpath/tck/AbstractIT.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ abstract class AbstractIT {
static final private String webappUrlPortSuffix = toPortSuffix(webappUrlScheme, webappUrlPort)
static final private String defaultWebappBaseUrl = "$webappUrlScheme://$webappUrlHost$webappUrlPortSuffix"
static final String webappBaseUrl = getVal("STORMPATH_TCK_WEBAPP_URL", defaultWebappBaseUrl)
static final String fromEmailDomain = getVal("STORMPATH_TCK_EMAIL_DOMAIN", "stormpath.com")

static final private List<String> possibleCSRFKeys = ['_csrf', 'csrfToken', 'authenticity_token', 'st']

Expand Down
28 changes: 20 additions & 8 deletions src/main/groovy/com/stormpath/tck/authentication/CookieIT.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import static com.stormpath.tck.util.TestAccount.Mode.WITHOUT_DISPOSABLE_EMAIL
import static org.hamcrest.Matchers.is
import static org.hamcrest.Matchers.not
import static org.testng.Assert.assertEquals
import static org.testng.Assert.assertNotNull
import static org.testng.Assert.assertTrue

class CookieIT extends AbstractIT {
Expand Down Expand Up @@ -68,8 +69,14 @@ class CookieIT extends AbstractIT {
.extract()
.response()

assertTrue(isCookieDeleted(response.detailedCookies.get("access_token")))
assertTrue(isCookieDeleted(response.detailedCookies.get("refresh_token")))
def accessTokenCookie = response.detailedCookies.get("access_token")
def refreshTokenCookie = response.detailedCookies.get("refresh_token")

assertNotNull(accessTokenCookie, "Cookie 'access_token'")
assertNotNull(refreshTokenCookie, "Cookie 'refresh_token")

assertTrue(isCookieDeleted(accessTokenCookie))
assertTrue(isCookieDeleted(refreshTokenCookie))
}

/** Reject unauthorized text/html requests with 302 to login route
Expand Down Expand Up @@ -131,12 +138,17 @@ class CookieIT extends AbstractIT {
}

def refreshTokenCookie = response.detailedCookies.get("refresh_token")
def refreshTokenTtl = JwtUtils.parseJwt(refreshTokenCookie.value).getBody().getExpiration().time
// some integrations use max-age and some use expires
if (refreshTokenCookie.expiryDate) {
assertEquals refreshTokenCookie.expiryDate.time, refreshTokenTtl
} else {
assertTrue refreshTokenCookie.maxAge * 1000L + now - refreshTokenTtl < 2000
assertNotNull(refreshTokenCookie)

// Okta does NOT use a JWT for the refresh token
if (refreshTokenCookie.getValue().split("\\.").length == 3) {
def refreshTokenTtl = JwtUtils.parseJwt(refreshTokenCookie.value).getBody().getExpiration().time
// some integrations use max-age and some use expires
if (refreshTokenCookie.expiryDate) {
assertEquals refreshTokenCookie.expiryDate.time, refreshTokenTtl
} else {
assertTrue refreshTokenCookie.maxAge * 1000L + now - refreshTokenTtl < 2000
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/groovy/com/stormpath/tck/errors/ErrorsIT.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.testng.annotations.Test
import static com.jayway.restassured.RestAssured.given
import static com.stormpath.tck.util.FrameworkConstants.MeRoute
import static com.stormpath.tck.util.FrameworkConstants.MissingRoute
import static org.hamcrest.core.StringStartsWith.startsWith;

class ErrorsIT extends AbstractIT {

Expand Down Expand Up @@ -52,6 +53,6 @@ class ErrorsIT extends AbstractIT {
.then()
.statusCode(401)
// 401 with Accept JSON header does not return JSON
.header("WWW-Authenticate", "Bearer realm=\"My Application\"")
.header("WWW-Authenticate", startsWith("Bearer"))
}
}
13 changes: 9 additions & 4 deletions src/main/groovy/com/stormpath/tck/forgot/ChangePasswordIT.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,7 @@ class ChangePasswordIT extends AbstractIT {
.then()
.statusCode(200)

// TODO - will need to make this configurable for Okta
String rawChangePasswordEmail = account.getEmail("stormpath.com")
String rawChangePasswordEmail = account.getEmail(fromEmailDomain)
String changePasswordHref = StringUtils.extractChangePasswordHref(rawChangePasswordEmail, "sptoken")

def response = given()
Expand Down Expand Up @@ -229,14 +228,17 @@ class ChangePasswordIT extends AbstractIT {
deleteOnClassTeardown(account.href)

given()
.contentType(ContentType.JSON)
.body([email: account.email])
.when()
.post(ForgotRoute)
.then()
.statusCode(200)

// TODO - will need to make this configurable for Okta
String rawChangePasswordEmail = account.getEmail("stormpath.com")
// sleep between requests okta rate limiting
Thread.sleep(1000)

String rawChangePasswordEmail = account.getEmail(fromEmailDomain)
String changePasswordHref = StringUtils.extractChangePasswordHref(rawChangePasswordEmail, "sptoken")
String sptoken = StringUtils.extractTokenFromHref(changePasswordHref, "sptoken")

Expand All @@ -252,6 +254,9 @@ class ChangePasswordIT extends AbstractIT {
.statusCode(200)
.body(isEmptyOrNullString())

// sleep between requests okta rate limiting
Thread.sleep(1000)

// Verify that the password is now the new password through a login attempt / OAuth token request
given()
.param("grant_type", "password")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class FacebookSocialLoginIT extends AbstractIT {
* Attempts to login with the Facebook Access Token, and expects an account object back.
* @throws Exception
*/
@Test(groups = ["v100", "json"])
@Test(groups = ["v100", "json", "stormpath_only"])
void loginWithValidFacebookAccessTokenSucceeds() throws Exception {
def loginJSON = ["providerData": [
"providerId": "facebook",
Expand All @@ -87,7 +87,7 @@ class FacebookSocialLoginIT extends AbstractIT {
* Attempts to login with an invalid access token, and should fail.
* @throws Exception
*/
@Test(groups = ["v100", "json"])
@Test(groups = ["v100", "json", "stormpath_only"])
void loginWithInvalidFacebookAccessTokenFails() throws Exception {
def loginJSON = ["providerData": [
"providerId": "facebook",
Expand All @@ -108,7 +108,7 @@ class FacebookSocialLoginIT extends AbstractIT {
* Attempts to use grant_type=stormpath_social with the Facebook Access Token, and expects an access_token back.
* @throws Exception
*/
@Test(groups = ["v100", "json"])
@Test(groups = ["v100", "json", "stormpath_only"])
void loginWithGrantTypeStormpathSocialSucceeds() throws Exception {

given()
Expand Down
21 changes: 14 additions & 7 deletions src/main/groovy/com/stormpath/tck/oauth2/Oauth2IT.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ import static org.hamcrest.Matchers.is
import static org.hamcrest.Matchers.isEmptyOrNullString
import static org.hamcrest.Matchers.not
import static org.hamcrest.Matchers.nullValue
import static org.testng.Assert.assertNotEquals
import static org.testng.Assert.assertTrue
import static org.testng.Assert.*

class Oauth2IT extends AbstractIT {

Expand Down Expand Up @@ -128,7 +127,7 @@ class Oauth2IT extends AbstractIT {
.extract()
.path("access_token")

assertTrue(JwtUtils.extractJwtClaim(accessToken, "sub") == account.href)
assertTrue isAccountSubInClaim(account, accessToken)
}

/** Password grant flow with username/password and access_token cookie present
Expand All @@ -142,6 +141,9 @@ class Oauth2IT extends AbstractIT {
def account = createTestAccount()
def cookies = createSession(account)

// Okta rate limiting on user/password requests
Thread.sleep(1000)

// @formatter:off
String accessToken =
given()
Expand All @@ -153,10 +155,10 @@ class Oauth2IT extends AbstractIT {
.post(OauthRoute)
.then()
.spec(JsonResponseSpec.validAccessAndRefreshTokens())
.extract()
.extract()
.path("access_token")
// @formatter:on
assertTrue(JwtUtils.extractJwtClaim(accessToken, "sub") == account.href)
assertTrue isAccountSubInClaim(account, accessToken)
}

/** Password grant flow with email/password
Expand All @@ -178,7 +180,7 @@ class Oauth2IT extends AbstractIT {
.extract()
.path("access_token")

assertTrue(JwtUtils.extractJwtClaim(accessToken, "sub") == account.href)
assertTrue isAccountSubInClaim(account, accessToken)
}

/** Refresh grant flow
Expand Down Expand Up @@ -215,7 +217,7 @@ class Oauth2IT extends AbstractIT {
.path("access_token")

assertNotEquals(accessToken, newAccessToken, "The new access token should not equal to the old access token")
assertTrue(JwtUtils.extractJwtClaim(accessToken, "sub") == account.href, "The access token should be a valid jwt for the test user")
assertTrue isAccountSubInClaim(account, accessToken)
}

/** Refresh grant flow should fail without valid refresh token
Expand Down Expand Up @@ -345,4 +347,9 @@ class Oauth2IT extends AbstractIT {
.contentType(ContentType.JSON)
.body("error", is("invalid_client"))
}

private boolean isAccountSubInClaim(def account, String jwt) {
def sub = JwtUtils.extractJwtClaim(jwt, "sub")
return account.href == sub || account.email == sub
}
}
9 changes: 8 additions & 1 deletion src/main/groovy/com/stormpath/tck/util/EnvUtils.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,20 @@ class EnvUtils {
public static final String jwtSigningKey
public static final String facebookClientId
public static final String facebookClientSecret
public static final String jwtSigningKeysUrl

static {
jwtSigningKeysUrl = getVal("STORMPATH_TCK_VALIDATE_JWT_URL")
jwtSigningKey = getVal("JWT_SIGNING_KEY")
facebookClientId = getVal("FACEBOOK_CLIENT_ID")
facebookClientSecret = getVal("FACEBOOK_CLIENT_SECRET")

if (jwtSigningKeysUrl == null && jwtSigningKey == null) {
fail("One of JWT_SIGNING_KEY or STORMPATH_TCK_VALIDATE_JWT_URL environment variables is required")
}

if (jwtSigningKey == null || facebookClientId == null || facebookClientSecret == null) {
fail("JWT_SIGNING_KEY, FACEBOOK_CLIENT_ID and FACEBOOK_CLIENT_SECRET environment variables are required")
fail("FACEBOOK_CLIENT_ID and FACEBOOK_CLIENT_SECRET environment variables are required")
}
}

Expand Down
78 changes: 73 additions & 5 deletions src/main/groovy/com/stormpath/tck/util/JwtUtils.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,88 @@
*/
package com.stormpath.tck.util

import groovy.json.JsonSlurper
import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jws
import io.jsonwebtoken.JwsHeader
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SigningKeyResolver
import io.jsonwebtoken.lang.Assert
import org.apache.commons.codec.binary.Base64

import java.security.Key
import java.security.KeyFactory
import java.security.NoSuchAlgorithmException
import java.security.spec.InvalidKeySpecException
import java.security.spec.RSAPublicKeySpec

class JwtUtils {



static String extractJwtClaim(String jwt, String property) {
String secret = EnvUtils.jwtSigningKey
Claims claims = Jwts.parser().setSigningKey(secret.getBytes()).parseClaimsJws(jwt).getBody()
return (String) claims.get(property)
return parseJwt(jwt).getBody().get(property)
}

static Jws<Claims> parseJwt(String jwt) {
String secret = EnvUtils.jwtSigningKey
return Jwts.parser().setSigningKey(secret.getBytes()).parseClaimsJws(jwt)


if (EnvUtils.jwtSigningKeysUrl) {
return Jwts.parser().setSigningKeyResolver(new URLSigningKeyResolver(EnvUtils.jwtSigningKeysUrl)).parseClaimsJws(jwt)
}
else {
String secret = EnvUtils.jwtSigningKey
return Jwts.parser().setSigningKey(secret.getBytes()).parseClaimsJws(jwt)
}
}

private static class URLSigningKeyResolver implements SigningKeyResolver {
def json

URLSigningKeyResolver(String keysUrl) {
def jsonSlurper = new JsonSlurper()
json = jsonSlurper.parse(new URL(keysUrl))
}

@Override
Key resolveSigningKey(JwsHeader header, Claims claims) {
return getKey(header)
}

@Override
Key resolveSigningKey(JwsHeader header, String plaintext) {
return getKey(header)
}

private Key getKey(JwsHeader header) {
String keyId = header.getKeyId()
String keyAlgorithm = header.getAlgorithm()

if (!"RS256".equals(keyAlgorithm)) {
throw new UnsupportedOperationException("Only 'RS256' key algorithm is supported.")
}

def key = null
for (def keyElement : json.keys) {
if (keyId.equals(keyElement.kid)) {
key = keyElement
break
}
}
Assert.notNull(key, "Key with 'kid' of "+keyId+" could not be found.")

try {

BigInteger modulus = new BigInteger(1, Base64.decodeBase64(key.n))
BigInteger publicExponent = new BigInteger(1, Base64.decodeBase64(key.e))
return KeyFactory.getInstance("RSA").generatePublic(
new RSAPublicKeySpec(modulus, publicExponent))

} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException("Failed to load key Algorithm", e)
} catch (InvalidKeySpecException e) {
throw new UnsupportedOperationException("Failed to load key", e)
}
}
}
}
4 changes: 3 additions & 1 deletion src/main/groovy/com/stormpath/tck/util/TestAccount.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.jayway.restassured.http.ContentType

import static com.jayway.restassured.RestAssured.get
import static com.jayway.restassured.RestAssured.given
Expand Down Expand Up @@ -62,6 +63,7 @@ class TestAccount {

void registerOnServer() {
href = given()
.contentType(ContentType.JSON)
.body(getPropertiesMap())
.when()
.post(RegisterRoute)
Expand Down Expand Up @@ -97,7 +99,7 @@ class TestAccount {
String emailId = null
int count = 0

while (emailId == null && count++ < 30) {
while (emailId == null && count++ < 90) {
def jsonResponse =
get(GUERILLA_MAIL_BASE + "?f=get_email_list&offset=0&sid_token=" + guerillaEmail.getToken()).asString()

Expand Down