Skip to content
Permalink
Browse files
Reshaped anubis interface to separate signature management from resou…
…rce initialization and to make key rotation possible.
  • Loading branch information
mifosio-04-04-2018 committed Apr 10, 2017
1 parent 8d1fc8d commit 6a0459692922de25cc280e2a0866faa8cb1734e8
Showing 34 changed files with 684 additions and 445 deletions.
@@ -25,6 +25,10 @@ dependencies {
[group: 'com.google.code.gson', name: 'gson'],
[group: 'io.mifos.core', name: 'api', version: versions.frameworkapi]
)

testCompile(
[group: 'io.mifos.core', name: 'test', version: versions.frameworktest],
)
}

publishing {
@@ -23,10 +23,8 @@ public interface TokenConstants {
String NO_AUTHENTICATION = "N/A";
String PREFIX = "Bearer ";

String JWT_VERSION_CLAIM = "/mifos.io/version";
String JWT_SIGNATURE_TIMESTAMP_CLAIM = "/mifos.io/signatureTimestamp";
String JWT_CONTENT_CLAIM = "/mifos.io/tokenContent";

String VERSION = "1";

String REFRESH_TOKEN_COOKIE_NAME = "org.apache.fineract.refreshToken";
}
@@ -15,23 +15,20 @@
*/
package io.mifos.anubis.api.v1.client;

import io.mifos.anubis.api.v1.domain.ApplicationSignatureSet;
import io.mifos.anubis.api.v1.domain.PermittableEndpoint;
import io.mifos.anubis.api.v1.domain.Signature;
import io.mifos.anubis.api.v1.validation.ValidKeyTimestamp;
import io.mifos.core.api.util.InvalidTokenException;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.*;

import java.math.BigInteger;
import java.util.List;

@SuppressWarnings("WeakerAccess")
@FeignClient
public interface Anubis {
String TENANT_PUBLIC_KEY_MODULUS_HEADER = "X-Tenant-Public-Key-Modulus";
String TENANT_PUBLIC_KEY_EXPONENT_HEADER = "X-Tenant-Public-Key-Exponent";

@RequestMapping(
value = "/permittables",
method = RequestMethod.GET,
@@ -40,11 +37,33 @@ public interface Anubis {
)
List<PermittableEndpoint> getPermittableEndpoints();

@RequestMapping(value = "/signatures/{timestamp}", method = RequestMethod.POST,
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.ALL_VALUE})
ApplicationSignatureSet createSignatureSet(@PathVariable("timestamp") @ValidKeyTimestamp String timestamp,
@RequestBody Signature identityManagerSignature)
throws InvalidTokenException, TenantNotFoundException;

@RequestMapping(value = "/signatures/{timestamp}", method = RequestMethod.GET,
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.ALL_VALUE})
ApplicationSignatureSet getSignatureSet(@PathVariable("timestamp") String timestamp)
throws InvalidTokenException, TenantNotFoundException;

@RequestMapping(value = "/signatures/{timestamp}", method = RequestMethod.DELETE,
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.ALL_VALUE})
void deleteSignatureSet(@PathVariable("timestamp") String timestamp)
throws InvalidTokenException, TenantNotFoundException;

@RequestMapping(value = "/signatures/{timestamp}/application", method = RequestMethod.GET,
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.ALL_VALUE})
Signature getApplicationSignature(@PathVariable("timestamp") String timestamp)
throws InvalidTokenException, TenantNotFoundException;

@RequestMapping(value = "/initialize", method = RequestMethod.POST,
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.ALL_VALUE})
void initialize(
@RequestHeader(TENANT_PUBLIC_KEY_MODULUS_HEADER) BigInteger tenantKeyMod,
@RequestHeader(TENANT_PUBLIC_KEY_EXPONENT_HEADER) BigInteger tenantKeyExp)
throws InvalidTokenException, TenantNotFoundException;
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.ALL_VALUE})
void initializeResources() throws InvalidTokenException, TenantNotFoundException;
}
@@ -19,21 +19,23 @@
import feign.FeignException;
import feign.Response;
import feign.codec.ErrorDecoder;
import io.mifos.anubis.api.v1.domain.Signature;
import io.mifos.core.api.util.InvalidTokenException;
import org.apache.http.HttpStatus;

import java.lang.reflect.Method;
import java.math.BigInteger;

/**
* @author Myrle Krantz
*/
class InitializeErrorDecoder implements ErrorDecoder {
@Override public Exception decode(final String methodKey, final Response response) {
try {
final Method method = Anubis.class.getDeclaredMethod("initialize", BigInteger.class, BigInteger.class);
final String initializeMethodKey = Feign.configKey(Anubis.class, method);
if (initializeMethodKey.equals(methodKey))
final Method createSignatureSetMethod = Anubis.class.getDeclaredMethod("createSignatureSet", String.class, Signature.class);
final String createSignatureSetMethodKey = Feign.configKey(Anubis.class, createSignatureSetMethod);
final Method initializeResourcesMethod = Anubis.class.getDeclaredMethod("initializeResources");
final String initializeResourcesMethodKey = Feign.configKey(Anubis.class, initializeResourcesMethod);
if (createSignatureSetMethodKey.equals(methodKey) || initializeResourcesMethodKey.equals(methodKey))
{
if (response.status() == HttpStatus.SC_BAD_REQUEST)
return new IllegalArgumentException();
@@ -46,7 +48,7 @@ else if (response.status() == HttpStatus.SC_FORBIDDEN)
return FeignException.errorStatus(methodKey, response);
}
catch (final NoSuchMethodException e) {
throw new IllegalStateException("Could not find initialize method.");
throw new IllegalStateException("Could not find createSignatureSet method."); //TODO:
}
}
}
@@ -0,0 +1,91 @@
/*
* Copyright 2017 The Mifos Initiative.
*
* 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.mifos.anubis.api.v1.domain;

import io.mifos.anubis.api.v1.validation.ValidKeyTimestamp;

import javax.validation.Valid;
import java.util.Objects;

/**
* @author Myrle Krantz
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public class ApplicationSignatureSet {
@ValidKeyTimestamp
private String timestamp;
@Valid
private Signature applicationSignature;
@Valid
private Signature identityManagerSignature;

public ApplicationSignatureSet() {
}

public ApplicationSignatureSet(String timestamp, Signature applicationSignature, Signature identityManagerSignature) {
this.timestamp = timestamp;
this.applicationSignature = applicationSignature;
this.identityManagerSignature = identityManagerSignature;
}

public String getTimestamp() {
return timestamp;
}

public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}

public Signature getApplicationSignature() {
return applicationSignature;
}

public void setApplicationSignature(Signature applicationSignature) {
this.applicationSignature = applicationSignature;
}

public Signature getIdentityManagerSignature() {
return identityManagerSignature;
}

public void setIdentityManagerSignature(Signature identityManagerSignature) {
this.identityManagerSignature = identityManagerSignature;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ApplicationSignatureSet that = (ApplicationSignatureSet) o;
return Objects.equals(timestamp, that.timestamp) &&
Objects.equals(applicationSignature, that.applicationSignature) &&
Objects.equals(identityManagerSignature, that.identityManagerSignature);
}

@Override
public int hashCode() {
return Objects.hash(timestamp, applicationSignature, identityManagerSignature);
}

@Override
public String toString() {
return "ApplicationSignatureSet{" +
"timestamp='" + timestamp + '\'' +
", applicationSignature=" + applicationSignature +
", identityManagerSignature=" + identityManagerSignature +
'}';
}
}
@@ -0,0 +1,45 @@
/*
* Copyright 2017 The Mifos Initiative.
*
* 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.mifos.anubis.api.v1.validation;

import io.mifos.core.lang.DateConverter;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.time.DateTimeException;

/**
* @author Myrle Krantz
*/
@SuppressWarnings("WeakerAccess")
public class CheckKeyTimestamp implements ConstraintValidator<ValidKeyTimestamp, String> {
@Override
public void initialize(ValidKeyTimestamp constraintAnnotation) { }

@Override
public boolean isValid(final String value, final ConstraintValidatorContext context) {
if (value == null)
return false;
try {
final String timeString = value.replace('_', ':');
DateConverter.fromIsoString(timeString);
return true;
}
catch (final DateTimeException ignored) {
return false;
}
}
}
@@ -0,0 +1,38 @@
/*
* Copyright 2017 The Mifos Initiative.
*
* 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.mifos.anubis.api.v1.validation;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

/**
* @author Myrle Krantz
*/
@SuppressWarnings("unused")
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {CheckKeyTimestamp.class}
)
public @interface ValidKeyTimestamp {
String message() default "Invalid key timestamp.";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
@@ -0,0 +1,62 @@
/*
* Copyright 2017 The Mifos Initiative.
*
* 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.mifos.anubis.api.v1.validation;

import io.mifos.core.lang.DateConverter;
import org.junit.Assert;
import org.junit.Test;

import java.time.Clock;
import java.time.LocalDateTime;

/**
* @author Myrle Krantz
*/
public class CheckKeyTimestampTest {
@Test
public void testValid()
{
final CheckKeyTimestamp testSubject = new CheckKeyTimestamp();

String utcNowAsString = DateConverter.toIsoString(LocalDateTime.now(Clock.systemUTC()));
Assert.assertTrue(testSubject.isValid(utcNowAsString, null));
}

@Test
public void testNull()
{
final CheckKeyTimestamp testSubject = new CheckKeyTimestamp();

Assert.assertFalse(testSubject.isValid(null, null));
}


@Test
public void testGobbledyGook()
{
final CheckKeyTimestamp testSubject = new CheckKeyTimestamp();

Assert.assertFalse(testSubject.isValid("gobbledygook", null));
}

@Test
public void testInitializeDoesntThrowException()
{
final CheckKeyTimestamp testSubject = new CheckKeyTimestamp();

testSubject.initialize(null);
}
}
@@ -17,6 +17,7 @@
import io.mifos.anubis.api.v1.client.Anubis;
import io.mifos.anubis.api.v1.client.AnubisApiFactory;
import io.mifos.anubis.api.v1.client.TenantNotFoundException;
import io.mifos.anubis.api.v1.domain.Signature;
import io.mifos.anubis.example.simple.Example;
import io.mifos.anubis.example.simple.ExampleConfiguration;
import io.mifos.anubis.test.v1.TenantApplicationSecurityEnvironmentTestRule;
@@ -91,8 +92,12 @@ public void testBrokenToken()

try (final AutoSeshat ignored2 = new AutoSeshat(brokenSeshatToken)) {
final TenantApplicationSecurityEnvironmentTestRule securityMock = new TenantApplicationSecurityEnvironmentTestRule(testEnvironment);

final String keyTimestamp = securityMock.getSystemSecurityEnvironment().tenantKeyTimestamp();
final RSAPublicKey publicKey = securityMock.getSystemSecurityEnvironment().tenantPublicKey();
anubis.initialize(publicKey.getModulus(), publicKey.getPublicExponent());
final Signature signature = new Signature(publicKey.getModulus(), publicKey.getPublicExponent());

anubis.createSignatureSet(keyTimestamp, signature);
}

Assert.assertFalse("A call with a broken token should result in an exception thrown.", true);

0 comments on commit 6a04596

Please sign in to comment.