...
- getDevices - list keys associated with a user
- deleteDevices - delete a key associated with a key ID
Motivation
If an evil person attacker obtained a users Refresh Token, the person could exchange the token for an Access Token (AT) from the Authorization Server, and maliciously act as the user. Therefore the Refresh Token (RT) needs to be stored securely on a users device. A naive approach is to encrypt the Refresh Token with a user chosen password. This password is usually vulnerable to brute force attacks. Therefore, we encrypt the Refresh Token with a 128 bit key, which the user can access from a web service, in such a way, that the service locks the key, if numerous invalid attempts are made at accessing the key. Thus leaving brute force attacks on getting access to the key infeasible.
Most mobile devices have a safe storage managed by the OS and protected by bio-metrics biometrics (finger print etc.). We would like to use this storage in some way, in order for users to authenticate using biometrics instead of a user chosen password. A naive way of supporting this would be to store the Refresh Token here. This, however, leaves us with different ways of accessing the Refresh Token, depending on if the user is authenticating using password or biometrics. Another option is to store the user chosen password in the safe storage. Then the the The app could then have the user authenticate using bio-metricsbiometrics, retrieve the user chosen password, access the key from Keyservice, decrypt the Refresh Token and have the Authorization Server exchange this for an Access Token.
Since people generally only use one or two passwords across all their applications, if an evil person managed to access the user chosen password, this would be very bada mechanism is needed to reduce the likelihood of such a security breach. Therefore, storing the user chosen password on the device is not an option, not even encrypted in some way (an evil person attacker could also intercept the password after it is decrypted, when it is sent to Keyservice).
When the Keyservice generates a new Key, the service also generates a 128 bit long secret. This long secret can also be used to retrieve the users Key from Keyservice, in the same way as the user chosen password. By storing the long secret in the bio-metrically biometrically secured safe storage on a device, if an evil person attacker managed to access the safe storage, the potentially long list of other applications secured by the users password would not be compromised.
...
Or by authenticating using bio-metrics on a mobile device, and decrypt the Client Credentials and Refresh Token using the long secret:
The following section presents the API of KeyService
Swagger open api macro |
---|
{ "swagger":"2.0", "info":{ "description":"This is the api description of the key service api.</br></br>", "version":"1.0.4-SNAPSHOT", "title":"Key service API", "contact":{ "url":"https://www.trifork.com", "email":"ktv@trifork.com" }, "license":{ } }, "host":"keyservice.fut.trifork.com", "basePath":"/", "tags":[ { "name":"main-controller", "description":"Main Controller" }, { "name":"management-controller", "description":"Management Controller" } ], "paths":{ "/createKey":{ "post":{ "tags":[ "main-controller" ], "summary":"createKey - requesting a \"key\" to be generated at the server via the given secret", "operationId":"createKeyUsingPOST", "consumes":[ "application/json" ], "produces":[ "*/*" ], "parameters":[ { "in":"body", "name":"input", "description":"input", "required":true, "schema":{ "$ref":"#/definitions/CreateKeyInput" } } ], "responses":{ "200":{ "description":"Successfully created key(the result field should always be \"OK\")", "schema":{ "$ref":"#/definitions/KeyIdResultFirstTime" } }, "500":{ "description":"Cannot create key" } }, "deprecated":false } }, "/key":{ "post":{ "tags":[ "main-controller" ], "summary":"key - Tries to retrieve a key via the given secret", "operationId":"getKeyUsingPOST", "consumes":[ "application/json" ], "produces":[ "*/*" ], "parameters":[ { "in":"body", "name":"input", "description":"input", "required":true, "schema":{ "$ref":"#/definitions/GetKeyFromSecretInput" } } ], "responses":{ "200":{ "description":"Check status field for the result of the operation; if not ok ,the other fields will be excluded", "schema":{ "$ref":"#/definitions/KeyIdResultInterface" } } }, "deprecated":false } }, "/longKey":{ "post":{ "tags":[ "main-controller" ], "summary":"longKey - Tries to retrieve a key via the given long secret", "operationId":"getKeyFromLongSecretUsingPOST", "consumes":[ "application/json" ], "produces":[ "*/*" ], "parameters":[ { "in":"body", "name":"input", "description":"input", "required":true, "schema":{ "$ref":"#/definitions/GetKeyFromLongSecretInput" } } ], "responses":{ "200":{ "description":"Check status field for the result of the operation; if not ok ,the other fields will be excluded", "schema":{ "$ref":"#/definitions/KeyIdResultInterface" } } }, "deprecated":false } }, "/management/deleteDevice":{ "post":{ "tags":[ "management-controller" ], "summary":"deleteDevice - Attempts to delete the given keyId by the given CPR number as verification.", "operationId":"deleteDeviceForCprUsingPOST", "consumes":[ "application/json" ], "produces":[ "*/*" ], "parameters":[ { "in":"body", "name":"input", "description":"input", "required":true, "schema":{ "$ref":"#/definitions/DeleteDeviceForCprInput" } } ], "responses":{ "200":{ "description":"look at the status field to know the result of the operation", "schema":{ "$ref":"#/definitions/DeleteDeviceForCprOutput" } }, "401":{ "description":"Bad / wrong / missing JWT token" } }, "security":[ { "JWT":[ "global" ] } ], "deprecated":false } }, "/management/devices":{ "get":{ "tags":[ "management-controller" ], "summary":"devices - Lists devices under the given CPR number", "operationId":"getDevicesByCprUsingGET", "produces":[ "*/*" ], "responses":{ "200":{ "description":"The list will be empty if anything fails or if there are none; if there are it will contain them.", "schema":{ "$ref":"#/definitions/GetDevicesByCprOutput" } }, "401":{ "description":"Bad / wrong / missing JWT token" } }, "security":[ { "JWT":[ "global" ] } ], "deprecated":false } } }, "securityDefinitions":{ "JWT":{ "type":"apiKey", "name":"Authorization", "in":"header" } }, "definitions":{ "CreateKeyInput":{ "type":"object", "required":[ "clientName", "deviceName", "secret" ], "properties":{ "clientName":{ "type":"string", "description":"A name of the client (properly not user provided)" }, "deviceName":{ "type":"string", "description":"A name of the device (potentially user provided)" }, "secret":{ "type":"string", "description":"The raw user supplied secret (password / pin)" } }, "title":"CreateKeyInput", "description":"This is what is required to create a new key" }, "DeleteDeviceForCprInput":{ "type":"object", "required":[ "keyId" ], "properties":{ "keyId":{ "type":"string", "description":"The keyId of the device we are trying to delete" } }, "title":"DeleteDeviceForCprInput", "description":"The input for trying to delete a device" }, "DeleteDeviceForCprOutput":{ "type":"object", "required":[ "status" ], "properties":{ "status":{ "type":"string", "description":"The resulting status of the deletion.", "enum":[ "deleted", "failed", "notFound" ] } }, "title":"DeleteDeviceForCprOutput", "description":"The result of deleting a device (by CPR & keyId)" }, "DeviceByCpr":{ "type":"object", "required":[ "clientName", "deviceName", "keyId" ], "properties":{ "clientName":{ "type":"string", "description":"The user supplied name for the device that performed the registration" }, "deviceName":{ "type":"string", "description":"A device name (not user supplied)" }, "keyId":{ "type":"string", "description":"an \"id\" so just a string(it is secure random)" } }, "title":"DeviceByCpr", "description":"A Device registered under the given cpr number" }, "GetDevicesByCprOutput":{ "type":"object", "required":[ "devices" ], "properties":{ "devices":{ "type":"array", "description":"The devices belonging to the given CPR.", "items":{ "$ref":"#/definitions/DeviceByCpr" } } }, "title":"GetDevicesByCprOutput", "description":"The result of querying for devices given a CPR number." }, "GetKeyFromLongSecretInput":{ "type":"object", "required":[ "keyId", "longSecret" ], "properties":{ "keyId":{ "type":"string", "description":"The KeyId we are to lookup" }, "longSecret":{ "type":"string", "description":"The long secret that we want to use to authenticate via the given keyId" } }, "title":"GetKeyFromLongSecretInput", "description":"The input for when trying to authenticate via a long secret" }, "GetKeyFromSecretInput":{ "type":"object", "required":[ "keyId", "secret" ], "properties":{ "keyId":{ "type":"string", "description":"The keyId we are trying to authenticate via" }, "secret":{ "type":"string", "description":"The secret related to that keyId" } }, "title":"GetKeyFromSecretInput", "description":"The input for trying to authenticate via a given secret (for the given keyId)" }, "KeyIdResultFailed":{ "title":"KeyIdResultFailed", "allOf":[ { "$ref":"#/definitions/KeyIdResultInterface" }, { "type":"object", "required":[ "status" ], "properties":{ "status":{ "type":"string", "description":"The resulting status of the operation; it will never be OK", "enum":[ "KeyNotFound", "WrongSecret", "KeyIsLocked" ] } }, "title":"KeyIdResultFailed", "description":"A failed operation." } ], "description":"A failed operation." }, "KeyIdResultFirstTime":{ "type":"object", "required":[ "clientName", "deviceName", "keyId", "keyValue", "longSecret" ], "properties":{ "clientName":{ "type":"string", "description":"The user supplied name for the device that performed the registration" }, "deviceName":{ "type":"string", "description":"A device name (not user supplied)" }, "keyId":{ "type":"string", "description":"an \"id\" so just a string(it is secure random)" }, "keyValue":{ "type":"string", "description":"a 128 bit AES - base64 encoded key" }, "longSecret":{ "type":"string", "description":"a random generated long secret, that is more secure to store on a user device." } }, "title":"KeyIdResultFirstTime", "description":"A create key response (success)" }, "KeyIdResultInterface":{ "type":"object", "required":[ "status" ], "discriminator":"status", "properties":{ "status":{ "type":"string", "enum":[ "OK", "KeyNotFound", "WrongSecret", "KeyIsLocked" ] } }, "title":"KeyIdResultInterface", "description":"The result of an operation. Inspect the status to know how it went.\nSee either KeyIdResultSuccess (when it is successful) or KeyIdResultFailed (when it fails)" }, "KeyIdResultSuccess":{ "title":"KeyIdResultSuccess", "allOf":[ { "$ref":"#/definitions/KeyIdResultInterface" }, { "type":"object", "required":[ "clientName", "deviceName", "keyId", "keyValue" ], "properties":{ "clientName":{ "type":"string", "description":"The user supplied name for the device that performed the registration" }, "deviceName":{ "type":"string", "description":"A device name (not user supplied)" }, "keyId":{ "type":"string", "description":"an \"id\" so just a string(it is secure random)" }, "keyValue":{ "type":"string", "description":"a 128 bit AES - base64 encoded key" }, "status":{ "type":"string", "description":"The resulting status of the operation (will always be OK)", "enum":[ "OK" ] } }, "title":"KeyIdResultSuccess", "description":"A login response" } ], "description":"A login response" } } } |
...