Third party systems can send "keychains" to users of Oslonøkkelen who has verified their phone numbers. Most norwegians should hopefully recognize this pattern from Vipps. We recommend reading this document top-to-bottom at least once if you intend to integrate your system with Oslonøkkelen.

Terminology

Short explanation of various terms used in this documentation and api.

Term Explanation

Keychain

Personal permission granting access to certain doors to certain times

Keychain factory

Configuration in Oslonøkkelen responsible for creating personal keychains. Oslonøkkelen supports multiple different types of such factories for push / pull models.

Lookup key

Your system have to identify the recipient of the keychain somehow. We call this lookup key in the api. Right now we only support looking up profiles based on phone numbers, but we are considering adding support for email.

Link

Systems pushing permissions to Oslonøkkelen will often have a web page where the user can see details and manage their booking or reservation. If your system has a page like this you should add a deep link to that page with each request you push to Oslonøkkkelen.

Pushing keychains - step-by-step

The following explain (more or less in order) the steps you have to do before your system can push permissions to users of Oslonøkkelen.

Obtaining an api key

Your system will get an id and api key that must be added as http headers to each request to Oslonøkkelen backend. The api key should (obviously) be kept secret and only transferred over https.

api key

Configuring one or more keychain factories

A keychain factory is the mechanism in Oslonøkkelen responsible for creating personal keychains granting users access to various doors at the appropriate time. The api described in this document interacts with a certain type of keychain factory in Oslonøkkelen.

keychain factory overview

Timezones

Each keychain factory will expect push messages to use local time in a given timezone. The reason behind this choice is that the local time is probably more relevant for most bookings / appointments. Should Norway ever stop observing daylight saving time you probably don’t want your booked events to occur on a different time of day…​

Self service?

We are working on "self service" keychain factory configuration, but right now you have to order the factories you need.

Getting the recipient to verify a phone number in Oslonøkkelen

Before someone can receive personal keychains in Oslonøkkelen, that person will have to verify a phone number in the app. That phone number can later be used to send keychains from other systems. Using a semi public id like phone number makes it possible to send keychains to others.

phone overview

As more people verify their phone numbers we expect this to become less of a problem, but for a while it is really important that your system explains (in a nice way) that the recipient of the keychain must have a verified phone number in Oslonøkkelen. Something to keep in mind here is that the person using your system might not be the recipient of the keychain (example: someone booking a room on behalf of someone else).

Pushing new keychains

Pushing a permission is done by creating a PushKeychainRequest message (see protobuf messages below) and posting it to …​/api/keychainfactory/{keychain-factory-id}/push/{permission-id} with the http headers described above.

The keychain-factory-id identifies the keychain factory you configured in a previous step. The factory will decide what the new keychain can open. Any later changes to the factory will be reflected in all keychains generated by this factory. For example, adding a third door to a keychain factory with two doors will allow new and old keychains generated by this factory to open the third door.

The second parameter permission-id is pretty important. It must be unique within your system and can be used to update or delete the keychain at some later time. Most systems will probably already have an internal id like a booking reference that can be used.

push overview

Sharing keychains

Oslonøkkelen supports sharing of keychains through the app. To allow this, you need to set the canShare attribute to true for each recipient you wish to be able to share their keychain. Right now, shared keychains are tied to the original keychain. If the original keychain is revoked, so will the shared keychains be. In the future, we will return information on which keychains have been shared, and you will be able to manage them separately.

Listening for event callbacks

Oslonøkkelen supports pushing `KeychainEvent when something "interesting" happens to keychains created by factories owned by your system. It will work like illustrated in the sequence diagram below.

event push
We have not decided on the format yet, but your system can fetch the public key from a well known uri on Oslonøkkelen to verify the signature. The basic idea is that your system can verify that the event is from Oslonøkkelen.

Kotlin client

Right now we leverage jitpack.io for building the Kotlin client. The neat thing about jitpack is that it can build any commit on demand. The first time you fetch an artefact it might take some time as it has to be built.

Sample build.gradle.kts configuration:

repositories {
    mavenCentral() // You probably already have this..
    maven { setUrl("https://jitpack.io") } // Just need to add this!
}

dependencies {

    // You can find the latest version here:
    // https://jitpack.io/#oslokommune/oslonokkelen-keychain-push
    implementation("com.github.oslokommune.oslonokkelen-keychain-push:oslonokkelen-keychain-push-client:v0.2.5")
    implementation("com.github.oslokommune.oslonokkelen-keychain-push:oslonokkelen-keychain-push-client-ktor:v0.2.5")

    // The rest of your dependencies...
}

Creating a client…​

// Create your ktor http client instance using your preferred engine.
// You are responsible for configuring sensible timeouts, metrics, logging etc..
val ktorHttpClient = HttpClient(CIO)

// Configure credentials needed for authentication
val config = OslonokkelenKeychainPushClient.Config(
        baseUri = URI.create("https://citykey-api.k8s.oslo.kommune.no"),
        apiSecret = "pepperkake-pepperkake-123",
        systemId = "your-system-id"
)

// Finally create a keychain push client using your configured ktor client + configuration
val keychainPushClient = OslonokkelenKeychainPushKtorClient(
        client = ktorHttpClient,
        config = config
)

Protobuf messages

We use protobuf to encode messages.

syntax = "proto3";

package com.github.oslokommune.oslonokkelen.keychainpush.proto;

option java_outer_classname = "KeychainPushApi";
option java_multiple_files = false;

// Used by third party system to push permissions to Oslonøkkelen.
//
// POST https://.../api/keychainfactory/{keychain-factory-id}/push/{permission-id}
//
// If the permission is successfully stored it will reply with http 201 and no response body.
// On error the response should contain `PermissionPushErrorResponse`
message PushKeychainRequest {

  // How Oslonøkkelen can find the recipients of the permission.
  //
  // Important
  // ---------
  // Right now Oslonøkkelen only supports a single recipient.
  // Sending requests with 0 or > 1 recipient will return an invalid request.
  repeated Recipient recipients = 1;

  // Determines when Oslonøkkelen is allowed to grant access.
  // Must contain at least one entry.
  repeated Period periods = 2;

  // "Nice to know" information to be presented to the user.
  InformationForUser informationForUser = 3;

  // At least one of these options must be provided. The recipient of the permission must have confirmed
  // this in Oslonøkkelen before any permissions can be pushed from third party systems.
  message Recipient {
    oneof lookupKey {

      // Recipient phone number including country code.
      PhoneNumber phoneNumber = 1;

      // We might add support for looking up users based on email later
      // string emailAddress = 2;

    }
  }

  // Determines when the recipient can use the keychain to open doors.
  // Note that each "keychain factory" is configured with a timezone (Europe/Oslo in 99.999999% of the cases)
  // and `from` and `until` fields are local time for that timezone.
  message Period {

    // ISO-8601 - Example: 2020-12-15T15:00:00
    // Don't include utc offset!
    string from = 1;

    // ISO-8601 - Example: 2020-12-15T15:00:00
    // Don't include utc offset!
    string until = 2;

  }

  // This information does not affect the permission itself, but is presented
  // to use user in the app. Think of this as "nice to have" information.
  message InformationForUser {

    // Mandatory title.
    // Max length: 100
    //
    // Example: "Testveien 2A - 19th of May - #booking-123"
    string title = 1;

    // Useful information related to the permission. Could be a reminder
    // to do (or not do) something after entering a building.
    //
    // Max length: 2000
    string message = 2;

    // Markdown will look a lot nicer in the app.
    // The default type is text.
    TextContentType messageContentType = 3;

    // If this is a booking it might be nice to include a link to a page where the recipient
    // can see and modify the booking.
    string moreInfoLink = 4;

  }


  enum TextContentType {

    PLAIN_TEXT = 0;

    MARKDOWN = 1;

  }

  // Used to look up user profiles.
  message PhoneNumber {

    // Example: 47
    string countryCode = 1;

    string phoneNumber = 2;

  }

  // Empty for now, but we might want to add fields later..
  //
  // Content-Type: application/protobuf;type=push-ok
  message OkResponse {

  }

}
// Content-Type: application/protobuf;type=delete
message KeychainDeleteRequest {

  // Optional reason. Will show up in the app.
  string humanReadableReason = 1;


  // Empty for now, but we might want to add fields later..
  //
  // Content-Type: application/protobuf;type=delete-ok
  message OkResponse {

  }

}

// Content-Type: application/protobuf;type=factories-ls
message ListKeychainFactoriesRequest {

  // Content-Type: application/protobuf;type=factory-list
  message ListResponse {
    repeated FactorySummary summary = 1;
  }

  message FactorySummary {
    string id = 1;
    string title = 2;
  }
}

// Content-Type: application/protobuf;type=error
message ErrorResponse {

  // Says something about what went wrong and what can be done about it.
  ErrorCode errorCode = 1;

  // Optional debug message. This is _not_ intended for the end user, but it can
  // contain useful technical information for debugging problems.
  string technicalDebugMessage = 2;

  enum ErrorCode {

    // Unexpected error.
    UNKNOWN = 0;

    // Unable to find a user profile.
    // Recipient of permissions must verify phone or email address
    // before he or she can receive permissions from third party systems.
    MISSING_PROFILE = 1;

    // Id or key is wrong. Please don't try again without
    // fixing the credentials.
    INVALID_CREDENTIALS = 2;

  }

}

// Third party systems can use this to fetch information about a keychain factories
// owned by that system. This can be a convenient way to validate the configuration.
//
// GET: https://.../api/keychainfactory/{keychain-factory-id}
// Response content-type: application/protobuf;type=keychain-factory-info
message KeychainFactoryPushInfo {

  // Example: Europe/Oslo
  string timezoneId = 1;

}

// EXPERIMENTAL!
// -------------
// The general idea is that Oslonøkkelen can push events to third party systems as
// things happen to keychains created by keychains factories owned by the system.
//
// These events will be signed allowing third party systems to download the public key
// and verify that the event was actually sent by Oslonøkkelen and not someone pretending to be.
//
// Content-type: application/protobuf;type=event
message KeychainEvent {

  // Timestamp indicating when the event happened.
  // ISO-8601 - UTC
  //
  // Example:
  // 2021-04-27T13:07:07.769606557Z
  string timestamp = 1;

  // One of these will be set.
  oneof event {
    KeychainCreated keychainCreated = 5;
    KeychainUpdated keychainUpdated = 6;
    KeychainDeleted keychainDeleted = 7;
  }

  // A new keychain was created by a guard configuration
  // owned by your system. Receiving this event for an unknown
  // permission is a good way to detect api key abuse.
  message KeychainCreated {
    string keychainFactoryId = 1;
    string permissionId = 2;
  }

  // A keychain owned by your system has been updated.
  message KeychainUpdated {
    string keychainFactoryId = 1;
    string permissionId = 2;
  }

  // A keychain owned by your system has been deleted.
  message KeychainDeleted {
    string keychainFactoryId = 1;
    string permissionId = 2;
  }

}

CLI

The repository contains a really simple CLI for experimenting / debugging.

Fat jar

The simplest way to build the CLI is to create a fat jar.

./gradlew :oslonokkelen-keychain-push-cli:shadowJar
java -jar oslonokkelen-keychain-push-cli/build/libs/keychain-pusher.jar --help

Native binary

This CLI can be compiled into a native binary with GraalVM. We recommend SDKMan as a convenient way of installing and upgrading GraalVM.

./gradlew :oslonokkelen-keychain-push-cli:nativeCompile
cp oslonokkelen-keychain-push-cli/build/native/nativeCompile/keychain-pusher <some-folder-on-$PATH>

# Optional: Generate shell autocomplete script
# See https://ajalt.github.io/clikt/autocomplete/ for more details
_KEYCHAIN_PUSHER_COMPLETE=zsh keychain-pusher > /tmp/keychain-pusher-autocomplete
source /tmp/keychain-pusher-autocomplete

# If you placed the binary somewhere on $PATH you should be able to execute it like this.
keychain-pusher --help