Skip to content
Permalink
Browse files
implementing the jwt validation and finally pass tcks
  • Loading branch information
rmannibucau committed Apr 22, 2018
1 parent 1ac8c1b commit b97fd1857f618936084ebfeaf01d36f9c12df287
Showing 16 changed files with 531 additions and 39 deletions.
@@ -32,15 +32,11 @@

<properties>
<tck.version>1.1-SNAPSHOT</tck.version>

<geronimo.jpms.name>org.apache.geronimo.microprofile.jwtauth</geronimo.jpms.name>
</properties>

<dependencies>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-el-api</artifactId>
<version>9.0.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.config</groupId>
<artifactId>microprofile-config-api</artifactId>
@@ -77,6 +73,7 @@
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.8.21</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.meecrowave</groupId>
@@ -16,15 +16,20 @@
*/
package org.apache.geronimo.microprofile.impl.jwtauth.config;

import java.util.function.Supplier;

@FunctionalInterface
public interface GeronimoJwtAuthConfig {
String read(String value, String def);

static GeronimoJwtAuthConfig create() {
try {
return new JwtAuthConfigMpConfigImpl();
} catch (final NoClassDefFoundError | ExceptionInInitializerError cnfe) {
return new DefaultJwtAuthConfig();
}
final Supplier<GeronimoJwtAuthConfig> delegate = () -> {
try {
return new JwtAuthConfigMpConfigImpl();
} catch (final NoClassDefFoundError | ExceptionInInitializerError cnfe) {
return new DefaultJwtAuthConfig();
}
};
return new PrefixedConfig(delegate.get());
}
}
@@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.geronimo.microprofile.impl.jwtauth.config;

import javax.enterprise.inject.Vetoed;

@Vetoed
class PrefixedConfig implements GeronimoJwtAuthConfig {
private final GeronimoJwtAuthConfig delegate;

PrefixedConfig(final GeronimoJwtAuthConfig geronimoJwtAuthConfig) {
this.delegate = geronimoJwtAuthConfig;
}

@Override
public String read(final String value, final String def) {
return delegate.read("geronimo.jwt-auth." + value, def);
}
}
@@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.geronimo.microprofile.impl.jwtauth.io;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Properties;

public final class PropertiesLoader {
private PropertiesLoader() {
// no-op
}

public static Properties load(final String value) {
final Properties properties = new Properties();
try (final Reader reader = new StringReader(value)) {
properties.load(reader);
} catch (final IOException e) {
throw new IllegalArgumentException(e);
}
return properties;
}
}
@@ -20,20 +20,17 @@
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toSet;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Stream;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import org.apache.geronimo.microprofile.impl.jwtauth.config.GeronimoJwtAuthConfig;
import org.apache.geronimo.microprofile.impl.jwtauth.io.PropertiesLoader;

@ApplicationScoped
class GroupMapper {
@@ -44,18 +41,10 @@ class GroupMapper {

@PostConstruct
private void init() {
ofNullable(config.read("geronimo.jwt-auth.groups.mapping", null))
ofNullable(config.read("groups.mapping", null))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(s -> {
final Properties properties = new Properties();
try (final Reader reader = new StringReader(s)) {
properties.load(reader);
} catch (final IOException e) {
throw new IllegalArgumentException(e);
}
return properties;
})
.map(PropertiesLoader::load)
.ifPresent(props -> props.stringPropertyNames()
.forEach(k -> mapping.put(k, Stream.of(props.getProperty(k).split(","))
.map(String::trim)
@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.geronimo.microprofile.impl.jwtauth.jwt;

import java.net.HttpURLConnection;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.json.JsonNumber;
import javax.json.JsonObject;

import org.apache.geronimo.microprofile.impl.jwtauth.JwtException;
import org.apache.geronimo.microprofile.impl.jwtauth.config.GeronimoJwtAuthConfig;
import org.eclipse.microprofile.jwt.Claims;

@ApplicationScoped
public class DateValidator {
@Inject
private GeronimoJwtAuthConfig config;
private boolean expirationMandatory;
private boolean issuedAtTimeMandatory;
private long tolerance;

@PostConstruct
private void init() {
expirationMandatory = Boolean.parseBoolean(config.read("exp.required", "true"));
issuedAtTimeMandatory = Boolean.parseBoolean(config.read("iat.required", "true"));
tolerance = Long.parseLong(config.read("date.tolerance", "60000")); // 1mn
}

void checkInterval(final JsonObject payload) {
long now = -1;

final JsonNumber exp = payload.getJsonNumber(Claims.exp.name());
if (exp == null) {
if (expirationMandatory) {
throw new JwtException("No exp in the JWT", HttpURLConnection.HTTP_UNAUTHORIZED);
}
} else {
final long expValue = exp.longValue();
now = now();
if (expValue < now - tolerance) {
throw new JwtException("Token expired", HttpURLConnection.HTTP_UNAUTHORIZED);
}
}

final JsonNumber iat = payload.getJsonNumber(Claims.iat.name());
if (iat == null) {
if (issuedAtTimeMandatory) {
throw new JwtException("No iat in the JWT", HttpURLConnection.HTTP_UNAUTHORIZED);
}
} else {
final long iatValue = iat.longValue();
if (now < 0) {
now = now();
}
if (iatValue > now + tolerance) {
throw new JwtException("Token issued after current time", HttpURLConnection.HTTP_UNAUTHORIZED);
}
}
}

private long now() {
return System.currentTimeMillis() / 1000;
}
}
@@ -24,38 +24,86 @@

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReaderFactory;
import javax.json.JsonString;

import org.apache.geronimo.microprofile.impl.jwtauth.JwtException;
import org.apache.geronimo.microprofile.impl.jwtauth.config.GeronimoJwtAuthConfig;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;

@ApplicationScoped
public class JwtParser {
@Inject
private GeronimoJwtAuthConfig config;

@Inject
private KidMapper kidMapper;

@Inject
private DateValidator dateValidator;

@Inject
private SignatureValidator signatureValidator;

private JsonReaderFactory readerFactory;

private String defaultKid;
private String defaultAlg;
private String defaultTyp;
private boolean validateTyp;

@PostConstruct
private void init() {
readerFactory = Json.createReaderFactory(emptyMap());
defaultKid = config.read("jwt.header.kid.default", null);
defaultAlg = config.read("jwt.header.alg.default", "RS256");
defaultTyp = config.read("jwt.header.typ.default", "JWT");
validateTyp = Boolean.parseBoolean(config.read("jwt.header.typ.validate", "true"));
}

public JsonWebToken parse(final String jwt) {
// TODO
final String[] split = jwt.split("\\.");
if (split.length != 3) {
// fail
final int firstDot = jwt.indexOf('.');
if (firstDot < 0) {
throw new JwtException("JWT is not valid", HttpURLConnection.HTTP_BAD_REQUEST);
}
final int secondDot = jwt.indexOf('.', firstDot + 1);
if (secondDot < 0 || jwt.indexOf('.', secondDot + 1) > 0 || jwt.length() <= secondDot) {
throw new JwtException("JWT is not valid", HttpURLConnection.HTTP_BAD_REQUEST);
}

final String rawHeader = jwt.substring(0, firstDot);
final JsonObject header = loadJson(rawHeader);
if (validateTyp && !getAttribute(header, "typ", defaultTyp).equalsIgnoreCase("jwt")) {
throw new JwtException("Invalid typ", HttpURLConnection.HTTP_UNAUTHORIZED);
}
// sign, date validation etc but without lib please, use GeronimoJwtAuthConfig to read how in postconstruct

final byte[] token = Base64.getUrlDecoder().decode(split[1]);
final JsonObject json = readerFactory.createReader(new ByteArrayInputStream(token)).readObject();
final String issuer = json.getString(Claims.iss.name());
if ("INVALID_ISSUER".equals(issuer)) { // todo
final JsonObject payload = loadJson(jwt.substring(firstDot + 1, secondDot));
dateValidator.checkInterval(payload);

final String alg = getAttribute(header, "alg", defaultAlg);
final String kid = getAttribute(header, "kid", defaultKid);
if (!kidMapper.loadIssuer(kid).equals(payload.getString(Claims.iss.name()))) {
throw new JwtException("Invalid issuer", HttpURLConnection.HTTP_UNAUTHORIZED);
}
signatureValidator.verifySignature(alg, kidMapper.loadKey(kid), jwt.substring(0, secondDot), jwt.substring(secondDot + 1));

return new GeronimoJsonWebToken(jwt, payload);
}

private String getAttribute(final JsonObject payload, final String key, final String def) {
final JsonString json = payload.getJsonString(key);
final String value = json != null ? json.getString() : def;
if (value == null) {
throw new JwtException("No " + key + " in JWT", HttpURLConnection.HTTP_UNAUTHORIZED);
}
return value;
}

return new GeronimoJsonWebToken(jwt, json);
private JsonObject loadJson(final String src) {
return readerFactory.createReader(new ByteArrayInputStream(Base64.getUrlDecoder().decode(src))).readObject();
}
}

0 comments on commit b97fd18

Please sign in to comment.