| /* |
| * 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.meecrowave.oauth2; |
| |
| import static java.util.Collections.singletonList; |
| import static javax.ws.rs.client.Entity.entity; |
| import static javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED_TYPE; |
| import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import java.io.File; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Method; |
| import java.net.URISyntaxException; |
| import java.nio.charset.StandardCharsets; |
| import java.security.PublicKey; |
| import java.util.Base64; |
| import java.util.function.BiFunction; |
| import java.util.stream.Stream; |
| |
| import javax.cache.Cache; |
| import javax.cache.CacheManager; |
| import javax.cache.Caching; |
| import javax.cache.configuration.MutableConfiguration; |
| import javax.cache.spi.CachingProvider; |
| import javax.json.JsonObject; |
| import javax.json.JsonString; |
| import javax.json.bind.Jsonb; |
| import javax.json.bind.JsonbBuilder; |
| import javax.ws.rs.client.Client; |
| import javax.ws.rs.client.ClientBuilder; |
| import javax.ws.rs.client.WebTarget; |
| import javax.ws.rs.core.Form; |
| import javax.ws.rs.core.Response; |
| |
| import org.apache.cxf.common.classloader.ClassLoaderUtils; |
| import org.apache.cxf.message.Message; |
| import org.apache.cxf.rs.security.oauth2.common.ClientAccessToken; |
| import org.apache.cxf.rs.security.oauth2.common.OAuthAuthorizationData; |
| import org.apache.cxf.rs.security.oauth2.provider.OAuthJSONProvider; |
| import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants; |
| import org.apache.geronimo.microprofile.impl.jwtauth.config.GeronimoJwtAuthConfig; |
| import org.apache.geronimo.microprofile.impl.jwtauth.jwt.DateValidator; |
| import org.apache.geronimo.microprofile.impl.jwtauth.jwt.JwtParser; |
| import org.apache.geronimo.microprofile.impl.jwtauth.jwt.KidMapper; |
| import org.apache.geronimo.microprofile.impl.jwtauth.jwt.SignatureValidator; |
| import org.apache.meecrowave.Meecrowave; |
| import org.apache.meecrowave.junit.MeecrowaveRule; |
| import org.apache.meecrowave.oauth2.provider.JCacheCodeDataProvider; |
| import org.eclipse.microprofile.jwt.JsonWebToken; |
| import org.junit.BeforeClass; |
| import org.junit.ClassRule; |
| import org.junit.Test; |
| |
| public class OAuth2Test { |
| private static final File KEYSTORE = new File("target/OAuth2Test/keystore.jceks"); |
| private static PublicKey PUBLIC_KEY; |
| |
| @ClassRule |
| public static final MeecrowaveRule MEECROWAVE = new MeecrowaveRule( |
| new Meecrowave.Builder().randomHttpPort() |
| .user("test", "pwd").role("test", "admin") |
| // jwt requires more config |
| .property("oauth2-use-jwt-format-for-access-token", "true") |
| .property("oauth2-jwt-issuer", "myissuer") |
| // auth code support is optional so activate it |
| .property("oauth2-authorization-code-support", "true") |
| // auth code jose setup to store the tokens |
| .property("oauth2.cxf.rs.security.keystore.type", "jks") |
| .property("oauth2.cxf.rs.security.keystore.file", KEYSTORE.getAbsolutePath()) |
| .property("oauth2.cxf.rs.security.keystore.password", "password") |
| .property("oauth2.cxf.rs.security.keystore.alias", "alice") |
| .property("oauth2.cxf.rs.security.key.password", "pwd") |
| // auth code needs a logged pcp to work |
| .loginConfig(new Meecrowave.LoginConfigBuilder().basic()) |
| .securityConstraints(new Meecrowave.SecurityConstaintBuilder() |
| .authConstraint(true) |
| .addAuthRole("**") |
| .addCollection("secured", "/oauth2/authorize") |
| .addCollection("secured", "/oauth2/authorize/decision")), ""); |
| |
| @BeforeClass |
| public static void createKeyStore() throws Exception { |
| PUBLIC_KEY = Keystores.create(KEYSTORE); |
| } |
| |
| @Test |
| public void getPasswordTokenNoClient() { |
| final Client client = ClientBuilder.newClient().register(new OAuthJSONProvider()); |
| try { |
| final ClientAccessToken token = client.target("http://localhost:" + MEECROWAVE.getConfiguration().getHttpPort()) |
| .path("oauth2/token") |
| .request(APPLICATION_JSON_TYPE) |
| .post(entity( |
| new Form() |
| .param("grant_type", "password") |
| .param("username", "test") |
| .param("password", "pwd"), APPLICATION_FORM_URLENCODED_TYPE), ClientAccessToken.class); |
| assertNotNull(token); |
| assertEquals("Bearer", token.getTokenType()); |
| assertNotNull(token.getTokenKey()); |
| assertIsJwt(token.getTokenKey(), "__default"); |
| assertEquals(3600, token.getExpiresIn()); |
| assertNotEquals(0, token.getIssuedAt()); |
| assertNotNull(token.getRefreshToken()); |
| validateJwt(token); |
| } finally { |
| client.close(); |
| } |
| } |
| |
| @Test |
| public void getRefreshTokenNoClient() { |
| final Client client = ClientBuilder.newClient().register(new OAuthJSONProvider()); |
| try { |
| // password |
| final ClientAccessToken primary = client.target("http://localhost:" + MEECROWAVE.getConfiguration().getHttpPort()) |
| .path("oauth2/token") |
| .request(APPLICATION_JSON_TYPE) |
| .post(entity( |
| new Form() |
| .param("grant_type", "password") |
| .param("username", "test") |
| .param("password", "pwd"), APPLICATION_FORM_URLENCODED_TYPE), ClientAccessToken.class); |
| |
| // refresh |
| final ClientAccessToken token = client.target("http://localhost:" + MEECROWAVE.getConfiguration().getHttpPort()) |
| .path("oauth2/token") |
| .request(APPLICATION_JSON_TYPE) |
| .post(entity( |
| new Form() |
| .param("grant_type", "refresh_token") |
| .param("refresh_token", primary.getRefreshToken()), APPLICATION_FORM_URLENCODED_TYPE), ClientAccessToken.class); |
| assertNotNull(token); |
| assertEquals("Bearer", token.getTokenType()); |
| assertIsJwt(token.getTokenKey(), "__default"); |
| assertEquals(3600, token.getExpiresIn()); |
| assertNotEquals(0, token.getIssuedAt()); |
| assertNotNull(token.getRefreshToken()); |
| } finally { |
| client.close(); |
| } |
| } |
| |
| @Test |
| public void authorizationCode() throws URISyntaxException { |
| final int httpPort = MEECROWAVE.getConfiguration().getHttpPort(); |
| |
| createRedirectedClient(httpPort); |
| |
| final Client client = ClientBuilder.newClient() |
| .property(Message.MAINTAIN_SESSION, true) |
| .register(new OAuthJSONProvider()); |
| try { |
| final WebTarget target = client.target("http://localhost:" + httpPort); |
| |
| final Response authorization = target |
| .path("oauth2/authorize") |
| .queryParam(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE_GRANT) |
| .queryParam(OAuthConstants.RESPONSE_TYPE, OAuthConstants.CODE_RESPONSE_TYPE) |
| .queryParam(OAuthConstants.CLIENT_ID, "c1") |
| .queryParam(OAuthConstants.CLIENT_SECRET, "cpwd") |
| .queryParam(OAuthConstants.REDIRECT_URI, "http://localhost:" + httpPort + "/redirected") |
| .request(APPLICATION_JSON_TYPE) |
| .header("authorization", "Basic " + Base64.getEncoder().encodeToString("test:pwd".getBytes(StandardCharsets.UTF_8))) |
| .get(); |
| final OAuthAuthorizationData data = authorization.readEntity(OAuthAuthorizationData.class); |
| assertNotNull(data.getAuthenticityToken()); |
| assertEquals("c1", data.getClientId()); |
| assertEquals("http://localhost:" + httpPort + "/oauth2/authorize/decision", data.getReplyTo()); |
| assertEquals("code", data.getResponseType()); |
| assertEquals("http://localhost:" + httpPort + "/redirected", data.getRedirectUri()); |
| |
| final Response decision = target |
| .path("oauth2/authorize/decision") |
| .queryParam(OAuthConstants.SESSION_AUTHENTICITY_TOKEN, data.getAuthenticityToken()) |
| .queryParam(OAuthConstants.AUTHORIZATION_DECISION_KEY, "allow") |
| .request(APPLICATION_JSON_TYPE) |
| .cookie(authorization.getCookies().get("JSESSIONID")) |
| .header("authorization", "Basic " + Base64.getEncoder().encodeToString("test:pwd".getBytes(StandardCharsets.UTF_8))) |
| .get(); |
| assertEquals(Response.Status.SEE_OTHER.getStatusCode(), decision.getStatus()); |
| assertTrue(decision.getLocation().toASCIIString(), decision.getLocation().toASCIIString().startsWith("http://localhost:" + httpPort + "/redirected?code=")); |
| |
| final ClientAccessToken token = target |
| .path("oauth2/token") |
| .request(APPLICATION_JSON_TYPE) |
| .post(entity( |
| new Form() |
| .param(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE_GRANT) |
| .param(OAuthConstants.CODE_RESPONSE_TYPE, decision.getLocation().getRawQuery().substring("code=".length())) |
| .param(OAuthConstants.REDIRECT_URI, "http://localhost:" + httpPort + "/redirected") |
| .param(OAuthConstants.CLIENT_ID, "c1") |
| .param(OAuthConstants.CLIENT_SECRET, "cpwd"), APPLICATION_FORM_URLENCODED_TYPE), ClientAccessToken.class); |
| assertNotNull(token); |
| assertEquals("Bearer", token.getTokenType()); |
| assertIsJwt(token.getTokenKey(), "c1"); |
| assertEquals(3600, token.getExpiresIn()); |
| assertNotEquals(0, token.getIssuedAt()); |
| assertNotNull(token.getRefreshToken()); |
| } finally { |
| client.close(); |
| } |
| } |
| |
| private void createRedirectedClient(final int httpPort) throws URISyntaxException { |
| final CachingProvider provider = Caching.getCachingProvider(); |
| final CacheManager cacheManager = provider.getCacheManager( |
| ClassLoaderUtils.getResource("default-oauth2.jcs", OAuth2Test.class).toURI(), |
| Thread.currentThread().getContextClassLoader()); |
| Cache<String, org.apache.cxf.rs.security.oauth2.common.Client> cache; |
| try { |
| cache = cacheManager |
| .createCache(JCacheCodeDataProvider.CLIENT_CACHE_KEY, new MutableConfiguration<String, org.apache.cxf.rs.security.oauth2.common.Client>() |
| .setTypes(String.class, org.apache.cxf.rs.security.oauth2.common.Client.class) |
| .setStoreByValue(true)); |
| } catch (final RuntimeException re) { |
| cache = cacheManager.getCache(JCacheCodeDataProvider.CLIENT_CACHE_KEY, String.class, org.apache.cxf.rs.security.oauth2.common.Client.class); |
| } |
| final org.apache.cxf.rs.security.oauth2.common.Client value = new org.apache.cxf.rs.security.oauth2.common.Client("c1", "cpwd", true); |
| value.setRedirectUris(singletonList("http://localhost:" + httpPort + "/redirected")); |
| cache.put("c1", value); |
| } |
| |
| private void assertIsJwt(final String tokenKey, final String client) { |
| final String[] split = tokenKey.split("\\."); |
| assertEquals(3, split.length); |
| final BiFunction<Jsonb, String, JsonObject> read = (jsonb, value) -> |
| jsonb.fromJson(new String(Base64.getUrlDecoder().decode(value), StandardCharsets.UTF_8), JsonObject.class); |
| try (final Jsonb jsonb = JsonbBuilder.create()) { |
| final JsonObject header = read.apply(jsonb, split[0]); |
| final JsonObject payload = read.apply(jsonb, split[1]); |
| assertEquals("RS256", header.getString("alg")); |
| assertEquals("test", payload.getString("username")); |
| assertEquals(client, payload.getString("client_id")); |
| } catch (final Exception e) { |
| fail(e.getMessage()); |
| } |
| } |
| |
| private void validateJwt(final ClientAccessToken token) { |
| final JwtParser parser = new JwtParser(); |
| final KidMapper kidMapper = new KidMapper(); |
| final DateValidator dateValidator = new DateValidator(); |
| final SignatureValidator signatureValidator = new SignatureValidator(); |
| final GeronimoJwtAuthConfig config = (value, def) -> { |
| switch (value) { |
| case "issuer.default": |
| return "myissuer"; |
| case "jwt.header.kid.default": |
| return "defaultkid"; |
| case "public-key.default": |
| return Base64.getEncoder().encodeToString(PUBLIC_KEY.getEncoded()); |
| default: |
| return def; |
| } |
| }; |
| setField(kidMapper, "config", config); |
| setField(dateValidator, "config", config); |
| setField(parser, "config", config); |
| setField(signatureValidator, "config", config); |
| setField(parser, "kidMapper", kidMapper); |
| setField(parser, "dateValidator", dateValidator); |
| setField(parser, "signatureValidator", signatureValidator); |
| Stream.of(dateValidator, signatureValidator, kidMapper, parser).forEach(this::init); |
| final JsonWebToken jsonWebToken = parser.parse(token.getTokenKey()); |
| assertNotNull(jsonWebToken); |
| assertEquals("myissuer", jsonWebToken.getIssuer()); |
| assertEquals("test", JsonString.class.cast(jsonWebToken.getClaim("username")).getString()); |
| } |
| |
| private void init(final Object o) { |
| try { |
| final Method init = o.getClass().getDeclaredMethod("init"); |
| if (!init.isAccessible()) { |
| init.setAccessible(true); |
| } |
| init.invoke(o); |
| } catch (final Exception e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| |
| private void setField(final Object instance, final String field, final Object value) { |
| try { |
| final Field declaredField = instance.getClass().getDeclaredField(field); |
| if (!declaredField.isAccessible()) { |
| declaredField.setAccessible(true); |
| } |
| declaredField.set(instance, value); |
| } catch (final Exception e) { |
| throw new IllegalArgumentException(e); |
| } |
| } |
| } |