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

Idea: Hacking up some changes to the TCK for Okta Support #331

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
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
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
42 changes: 27 additions & 15 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 @@ -105,18 +112,18 @@ class CookieIT extends AbstractIT {
saveCSRFAndCookies(LoginRoute)

def requestSpecification = given()
.accept(ContentType.JSON)
.contentType(ContentType.JSON)
.body([ "login": account.email, "password": account.password ])
.accept(ContentType.JSON)
.contentType(ContentType.JSON)
.body(["login": account.email, "password": account.password])

setCSRFAndCookies(requestSpecification, ContentType.JSON);

def response = requestSpecification
.when()
.when()
.post(LoginRoute)
.then()
.then()
.statusCode(200)
.extract()
.extract()
.response()

def now = new Date().time
Expand All @@ -127,16 +134,21 @@ class CookieIT extends AbstractIT {
if (accessTokenCookie.expiryDate) {
assertEquals accessTokenCookie.expiryDate.time, accessTokenTtl
} else {
assertTrue accessTokenCookie.maxAge * 1000L + now - accessTokenTtl < 2000
assertTrue accessTokenCookie.maxAge * 1000L + now - accessTokenTtl < 2000
}

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
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 @@ -235,8 +234,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")
String sptoken = StringUtils.extractTokenFromHref(changePasswordHref, "sptoken")

Expand Down
22 changes: 14 additions & 8 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,8 @@ class Oauth2IT extends AbstractIT {
def account = createTestAccount()
def cookies = createSession(account)

// UGGGG

// @formatter:off
String accessToken =
given()
Expand All @@ -153,10 +154,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 +179,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 +216,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 @@ -310,7 +311,7 @@ class Oauth2IT extends AbstractIT {
/** We shouldn't be able to use client credentials to get an access token without a API secret
* @see <a href="https://github.com/stormpath/stormpath-framework-tck/issues/8">#8</a>
*/
@Test(groups=["v100", "json"])
@Test(groups=["v100", "json", "client_credentials"])
void oauthClientCredentialsGrantFailsWithoutAPISecret() throws Exception {
// Get API keys so we can use it for client credentials

Expand Down Expand Up @@ -345,4 +346,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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nbarbettini Try this out

}
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)
}
}
}
}