/*
* Copyright (c) 2015 the original author or authors
*
* 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 io.werval.modules.jose;
import com.fasterxml.jackson.databind.JsonNode;
import io.werval.api.outcomes.Outcome;
import io.werval.modules.jose.filters.RequireRoles;
import io.werval.modules.jose.filters.RequireSubject;
import io.werval.runtime.routes.RoutesParserProvider;
import io.werval.test.WervalHttpRule;
import io.werval.util.Maps;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.junit.ClassRule;
import org.junit.Test;
import static com.jayway.restassured.RestAssured.given;
import static com.jayway.restassured.RestAssured.when;
import static io.werval.api.context.CurrentContext.application;
import static io.werval.api.context.CurrentContext.outcomes;
import static io.werval.api.context.CurrentContext.plugin;
import static io.werval.api.context.CurrentContext.request;
import static io.werval.api.http.Status.OK_CODE;
import static io.werval.api.http.Status.UNAUTHORIZED_CODE;
import static io.werval.api.mime.MimeTypesNames.APPLICATION_JSON;
import static io.werval.modules.json.JSON.json;
import static java.util.Collections.singletonMap;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* JwtPlugin Test.
*/
public class JwtPluginTest
{
@ClassRule
public static final WervalHttpRule WERVAL = new WervalHttpRule( new RoutesParserProvider(
"POST /login io.werval.modules.jose.JwtPluginTest$Controller.login\n"
+ "POST /renew JWT.renew\n"
+ "GET /authenticated io.werval.modules.jose.JwtPluginTest$Controller.authenticated\n"
+ "GET /authorized io.werval.modules.jose.JwtPluginTest$Controller.authorized\n"
) );
public static class Controller
{
public Outcome login()
{
JsonNode body = json().fromJSON( request().body().asBytes() );
String email = body.get( "email" ).asText();
String password = body.get( "password" ).asText();
if( "admin@example.com".equals( email ) && "admin-password".equals( password ) )
{
HashMap<String, Object> claims = Maps.newHashMap( String.class, Object.class )
.put( JWT.CLAIM_SUBJECT, email )
.put( "roles", Arrays.asList( "admin" ) )
.toMap();
String token = plugin( JWT.class ).tokenForClaims( claims );
return outcomes().ok()
.withHeader( application().config().string( JWT.HTTP_HEADER_CONFIG_KEY ), token )
.build();
}
return outcomes().unauthorized().build();
}
@RequireSubject
public Outcome authenticated()
{
return outcomes().ok().build();
}
@RequireRoles( "admin" )
public Outcome authorized()
{
return outcomes().ok().build();
}
}
@Test
public void api()
{
JWT jwt = WERVAL.application().plugin( JWT.class );
String token = jwt.tokenForClaims( singletonMap( JWT.CLAIM_SUBJECT, "someone@example.com" ) );
Map<String, Object> parsed = jwt.claimsOfToken( token );
assertThat( parsed.get( JWT.CLAIM_SUBJECT ), equalTo( "someone@example.com" ) );
}
@Test
public void http()
throws InterruptedException
{
String tokenHeaderName = WERVAL.application().config().string( JWT.HTTP_HEADER_CONFIG_KEY );
JWT jwt = WERVAL.application().plugin( JWT.class );
// Unauthorized access to authenticated resource
when().get( "/authenticated" )
.then().statusCode( UNAUTHORIZED_CODE );
// Login
String token = given().body( "{\"email\":\"admin@example.com\",\"password\":\"admin-password\"}" )
.contentType( APPLICATION_JSON )
.when().post( "/login" )
.then().statusCode( OK_CODE )
.header( tokenHeaderName, notNullValue() )
.log().all()
.extract().header( tokenHeaderName );
// Authenticated access
given().header( tokenHeaderName, token )
.when().get( "/authenticated" )
.then().statusCode( OK_CODE );
// Authorized access
given().header( tokenHeaderName, token )
.when().get( "/authorized" )
.then().statusCode( OK_CODE );
// Gather time related claims from token
ZoneId utc = ZoneId.of( "UTC" );
Map<String, Object> claims = jwt.claimsOfToken( token );
ZonedDateTime iat = ZonedDateTime.ofInstant(
Instant.ofEpochSecond( (Long) claims.get( JWT.CLAIM_ISSUED_AT ) ), utc
);
ZonedDateTime nbf = ZonedDateTime.ofInstant(
Instant.ofEpochSecond( (Long) claims.get( JWT.CLAIM_NOT_BEFORE ) ), utc
);
ZonedDateTime exp = ZonedDateTime.ofInstant(
Instant.ofEpochSecond( (Long) claims.get( JWT.CLAIM_EXPIRATION ) ), utc
);
// Wait at least one second before renewal so new dates will be different
Thread.sleep( 1200 );
// Renew token
String renewed = given().header( tokenHeaderName, token )
.when().post( "/renew" )
.then().statusCode( OK_CODE )
.header( tokenHeaderName, notNullValue() )
.log().all()
.extract().header( tokenHeaderName );
// Gather time related claims from renewed token
claims = jwt.claimsOfToken( renewed );
ZonedDateTime renewedIat = ZonedDateTime.ofInstant(
Instant.ofEpochSecond( (Long) claims.get( JWT.CLAIM_ISSUED_AT ) ), utc
);
ZonedDateTime renewedNbf = ZonedDateTime.ofInstant(
Instant.ofEpochSecond( (Long) claims.get( JWT.CLAIM_NOT_BEFORE ) ), utc
);
ZonedDateTime renewedExp = ZonedDateTime.ofInstant(
Instant.ofEpochSecond( (Long) claims.get( JWT.CLAIM_EXPIRATION ) ), utc
);
// Assert renewed token time related claims are greater than the ones in the original token
assertTrue( renewedIat.isAfter( iat ) );
assertTrue( renewedNbf.isAfter( nbf ) );
assertTrue( renewedExp.isAfter( exp ) );
}
}