Deploying a vIdP

An OLYMPUS virtual IdP (vIdP) consists of a number of components working together. Some of these are delivered as part of the OLYMPUS framework, whereas others are specific for a concrete application and must therefore be implemented by the application developer. The main components are the following:

  • Configuration handling

  • Identity proofing

  • Cryptomodule and storage

  • Multifactor authentication

  • REST interface deployment

This document introduces the various components, along with small samples. A full example can be found in the sourcecode as TODO and in the end of this page.

Configuration handling

The vIdP needs a number of parameters, such as key material, lifetime of issued tokens and information on the other servers, as part of the setup procedure. For full functionality of the distributed IdP, a PABCConfiguration should be used. This can either be provided as a custom implementation or using the standard PABCConfigurationImpl class provided by the OLYMPUS framework. The standard PABCConfigurationImpl can be provided using JSON (a full working set of configuration files can be found in test/resources):

       {"keyStorePath":"src/test/resources/keystore.jks",
        "keyStorePassword":"server1",
        "trustStorePath":"src/test/resources/keystore.jks",
        "trustStorePassword":"server1",
        "port":9080,
        "myAuthorizationCookie":"hk5NQgrO9BJlBedHaD8Qro8rc6SGL65H9W90Wt2snpr2cKwnSQ+C2bK55F2sKZRa0gxAkKz6P5mu3Nk9WKLGSg==",
        "authorizationCookies":{
        "5tabBLiLXuD1eWTTliSHvkfVLYr9+lhs0WhPpET3NGBCp3r4onpzRUh2/tyZvdW6fDp1yr2uAePpGPvXnhkwCw==":{"id":"server1", "roles":["SERVER"]},
        "WS66b+TWK3XHHLJW3YHJ3KGJk0wEfeTmxouRFVc+/w9hps+OcL2JwxVW5kK6wH1tD3jAmD18yv/6fs3308vYDw==":{"id":"server2", "roles":["SERVER"]},
        "8Y9mocwbGZbU0YSNQR46kb6DHYuniHqpXmOjM2uUQ+iEmlX/ka4ZPzBjgrWz9Zw/zeNA4Neq9LSLAaPa6+B0Vg==":{"id":"administrator", "roles":["ADMIN"]}},
        "servers":["http://127.0.0.1:9081","http://127.0.0.1:9082"],
        "tlsPort":9933,
        "keyMaterial": {"modulus":209145188754454650...4757169531007361728238634367542988141617, "privateKey":1462249078153...0513422592, "publicExponent":65537},
        "oprfBlindings":{"1":162683617192684354721534228919969509586,"2":201103837588655148085398197875718785305},
        "rsaBlindings":{"1":116250903442045051614123370266866563879,"2":139820269263465496115751481575796568221},
        "localKeyShare":"56tnFNMZfBX/...fqJHkH4y4eVEcvadnSv3iqmS6v7plckHPasX5kpxPqbmg07Zkb/WO0hrWWif08o/ceGRW9m9yadC5UG0l4aQuA==",
        "remoteShares":{"1":"iHOmSPB1DxgImGYJTv1rKk2wPZ4pMRa3...3FpFdBSuRLxtslThgNqn5Bkhhx8y6NXkWL/NXmC2F7yGxgn75+Q==", "2":"nhVAQ6gKlD8XxfjPMLLM+rbW39Iex3...UU5m8FPeYygiYkEJJCchTHn33dA=="},
        "oprfKey":2272631655901059652376853502653585612556037362756263395289688822267433693845509915514559918780126188577715083976063300998323278426691654597,
        "refreshKey":"AAAAAA==",
        "id":0,
        "waitTime":1000,
        "attrDefinitions":[ {"id":"url:PurchaseTime", "shortName":"Time of Purchase", "minDate":"2000-01-01T00:00:00", "maxDate":"2021-02-01T00:00:00", "type":"Date"}, 
          {"id":"url:DateOfBirth", "shortName":"Date of Birth", "minDate":"1950-01-01T00:00:00", "maxDate":"2021-09-01T00:00:00", "granularity":"DAYS", "type":"Date"}
          {"id":"url:IntegerWithNegatives", "shortName":"Int", "minimumValue":-20, "maximumValue":300, "type":"Integer"},
          {"id":"url:HasDrivingPermit", "shortName":"Has Driving Permit", "type":"Boolean"}, 
          {"id":"url:FamilyName", "shortName":"Family Name", "minLength":2, "maxLength":16, "type":"String"} ],
        "seed":"cmFuZG9tIHZhbHVlIHJhbmRvbSB2YWx1ZSByYW5kb20gdmFsdWUgcmFuZG9tIHZhbHVlIHJhbmRvbQ==",
        "lifetime":72000000,
        "allowedTimeDifference":10000}

Identity proofing

Identity proofing is the process of getting user attributes into the vIdP. After a user account has been created, a user may send an IdentityProof to the vIdP. If the vIdP can validate the proof, the attributes in the IdentityProof is then stored as part of the user’s account. The IdentityProof is essentially just a key-value mapping of attributes along with some kind of signature. This can be implemented done in many different ways, using JWT, signed XML or many other schemes, and is left up to the application developer to implement.

For each identity proofing scheme the application should support, the application develop must implement two components, an implementation of the IdentityProver interface and a matching implementation of the IdentityProof interface. The following is a very basic example, using a key-value map of attributes along with a signature string as an IdentityProof and a matching IdentityProver, that verifies that the signature is “SIGNATURE” and stores the attributes.

  • IdentityProof:

       @JsonTypeInfo(use=Id.CLASS, include=As.PROPERTY, property="@class")
       public class DemoIdentityProof extends IdentityProof {
          private String signature;
          @JsonTypeInfo(use=Id.CLASS, include=As.PROPERTY, property="class")
          private Map<String, Attribute> attributes;
       
          public DemoIdentityProof() {
          }
       
          public DemoIdentityProof(String signature, Map<String, Attribute> attributes) {
             super();
             this.signature = signature;
             this.attributes = attributes;
          }

          public Object getSignature() {
             return signature;
          }
          
          public void setSignature(String signature) {
             this.signature = signature;
          }
       
          public Map<String, Attribute> getAttributes() {
             return attributes;
          }

          public void setAttributes(Map<String, Attribute> attributes) {
             this.attributes = attributes;
          }
       }
  • IdentityProver:

       public class DemoIdentityProver implements IdentityProver {

          private Storage storage; 

          public DemoIdentityProver(Storage storage) {
             this.storage = storage;
          }

          //Only validates that the proof is a TokenIdentityProof
          @Override
          public boolean isValid(String input, String username) {
             ObjectMapper mapper = new ObjectMapper();
                try {
                   mapper.readValue(input, DemoIdentityProof.class);
                } catch (IOException e) {
                   return false;
                }
                return true;
          }

          @Override
          public void addAttributes(String input, String username) {
             ObjectMapper mapper = new ObjectMapper();
             DemoIdentityProof proof;
             try {
                proof = mapper.readValue(input, DemoIdentityProof.class);
                storage.addAttributes(username, proof.getAttributes());
             } catch (IOException e) {
             }
          }
       }

Cryptomodule and storage

In some applications there may be requirements to storing sensitive information in special hardware. In order to support this, the application developer should make an implementation of the relevant storage interface (in order to persist data) and may implement a custom implementation of the ServerCryptoModule interface.

The storage interface allows the application developer to use their choice of database. The OLYMPUS framework does contain an InMemory implementation of the relevant interface, however as the name implies, this does not persist data after restarts.

The ServerCryptoModule allows integration with HSM modules or other strong cryptographic solutions. The OLYMPUS framework contains a software implementation of the interface and may be used if HSM support is not required.

The following code sample will create an InMemoryDatabase and a SoftwareServerCryptoModule:

       PestoDatabase db = new InMemoryPestoDatabase();
       ServerCryptoModule cryptoModule = new SoftwareServerCryptoModule(new SecureRandom());

Multifactor authentication

Some applications may require the use of multifactor authentication to provide an additional layer of security. Similar to the processes around identity proofing, this can be done in a number of ways and is therefore left for the application developer to implement. Each authenticator implementation must implement the MFAAuthenticator interface, which specifies 4 main methods:

  • generateTOTP(String secret) - Given some private keymaterial, generate a one-time key.

  • isValid(String token, String secret) - Validate if the provided token is valid with regard to the key material.

  • generateSecret() - Generate private key material.

  • combineSecrets(List secrets) - Given a list of partial key material, combine it into a single key.

Note that since the vIdP is distributed, each partial IdP need to generate “trusted” key material, which is then combined to the key material that is actually used. In some cases, this may not be necessary, in which case the combineSecrets method should be implemented accordingly.

The following sample code shows how a simple TOTP based Authenticators could be implemented. Note that each type of authenticator should have some unique identifier, so the the clients can specify which type of authenticator is used:

       public class TOTPAuthenticator implements MFAAuthenticator {
       
          public static final String TYPE = "TOTP_AUTHENTICATOR";
          private static final int BYTES_IN_SECRET = 20;
          private final CommonCrypto crypto;
          private final Base32 base32 = new Base32();
       
          public TOTPAuthenticator(CommonCrypto crypto) {
             this.crypto = crypto;
          }
       
          @Override
          public boolean isValid(String token, String secret) {
             String totp = generateTOTP(secret);
             return totp.equals(token);	
          }
       
          @Override
          public String generateTOTP(String secret) {
             byte[] bytes = base32.decode(secret);
             String hexKey = Hex.encodeHexString(bytes);
             return TOTP.getOTP(hexKey);
          }
       
          @Override
          public String generateSecret() {
             return base32.encodeToString(crypto.getBytes(BYTES_IN_SECRET));
          }
       
          @Override
          public String combineSecrets(List<String> secrets) {
             byte[] combinedSecret = base32.decode(secrets.remove(0));
             while(secrets.size() > 0) {
                combinedSecret = Util.xorArray(combinedSecret, base32.decode(secrets.remove(0)));
             }
             Base32 base32 = new Base32();
             return base32.encodeToString(combinedSecret);
          }
       }

REST based deployment

The OLYMPUS framework offers a basic REST server (RESTIdPServer), allowing for the vIdP functionality to be exposed as a series of REST endpoints. The RESTIdPServer takes an VirtualIdP as argument and is afterwards configured with relevant ports for HTTP/HTTPS communication as well as a trust- and keystores for TLS connectivity.

       RESTIdPServer restServer = new RESTIdPServer();
       restServer.setIdP(idp);
       List<String> servlets = new ArrayList<>(1);
       servlets.add(PestoIdPServlet.class.getCanonicalName());
       String keyStorePath = "some/path";
       restServer.start(8080, servlets, 443, keyStorePath, "keyStorePassword", "trustStorePassword");

Complete example

The following tutorial code shows how a partial IdP may be started using components included in the framework. Note that it is uses the previously described DemoIdentityProvider and TOTPAuthenticator. While the TOTPAuthenticator is “secure”, the DemoIdentityProvider does no validation and should only be used for testing. A slight modified version of the example code may also be found in the source code as the RunOIDCServer or RunCFPServer classes in the oidc-demo-idp and cfp-usecase projects.

    public class RunTestServer {

          /**
           * Main method to start a server. Takes the path to a configuration file as a paramenter. If no paramenters
           * are given, it looks for the location of the configuration file in the ENV variable CONFIG_FILE
           */
          public static void main(String[] args) throws Exception {
             ObjectMapper mapper = new ObjectMapper();
             PABCConfigurationImpl configuration = null;
             if (args.length == 0) {
                String configFile = System.getenv("CONFIG_FILE");
                configuration = mapper.readValue(new File(configFile), PABCConfigurationImpl.class);
             } else {
                configuration = mapper.readValue(new File(args[0]), PABCConfigurationImpl.class);
             }

             List<PestoIdP> others = new ArrayList<PestoIdP>();
             for (String s: configuration.getServers()) {
                others.add(new PestoIdP2IdPRESTConnection(s, configuration.getId(), configuration.getKeyStorePath(), configuration.getKeyStorePassword(),
                   configuration.getTrustStorePath(), configuration.getTrustStorePassword(), configuration.getMyAuthorizationCookie()));
             }

             //Setup databases
             PestoDatabase db = new InMemoryPestoDatabase();

             //Setup identity provers
             List<IdentityProver> identityProvers = new LinkedList<IdentityProver>();
             identityProvers.add(new DemoIdentityProver(db));

             ServerCryptoModule cryptoModule = new SoftwareServerCryptoModule(new SecureRandom());

             List<String> types = new ArrayList<>(1);
             types.add(PestoIdPServlet.class.getCanonicalName());

             //Setup the IdP.
             OIDCPestoIdPImpl idp = null;
             Map<String, MFAAuthenticator> authenticators = new HashMap<>();
             authenticators.put(TOTPAuthenticator.TYPE, new TOTPAuthenticator());

             idp = new OIDCPestoIdPImpl(db, identityProvers, authenticators, cryptoModule);
             idp.setup("ssid", configuration, others);
             //And also an in memory database for authorization of servers and admins
             for(String cookie: configuration.getAuthorizationCookies().keySet()) {
                Authorization authorization = configuration.getAuthorizationCookies().get(cookie);
                authorization.setExpiration(System.currentTimeMillis()+604800000); //valid for one week
                idp.addSession(cookie, authorization);
             }
		
             RESTIdPServer restServer = new RESTIdPServer();
             restServer.setIdP(idp);
             try {
                restServer.start(configuration.getPort(), types,
                   configuration.getTlsPort(), 
                   configuration.getKeyStorePath(), 
                   configuration.getKeyStorePassword(), 
                   configuration.getTrustStorePassword());
             }catch(Exception e) {
          }
     }