README

Please don't hesitate to open an issue or write to contact@upsignon.eu if you have a question or if you think this doc could be improved. Your requests will be treated as high priority.

How UpSignOn works

Users download the UpSignOn application and store their data in it.

You add the UpSignOn button in your site and/or application. This button uses an app link to redirect users to UpSignOn with a request that you configure.

The configured request is displayed to the user to get his consent. The application can then redirect the user to your site or app while sending you the data you need.

Look at this scheme to understand how UpSignOn interacts with your server.

Display in full screen

Disclaimer

UpSignOn is not responsible for the correct implementation of this protocol.

All the routes you open on your server or application must be secured. UpSignOn provides best practices to mitigate security issues but they may not be relevant or sufficient depending on your context. Please open an issue if you think we should update our documentation.

Notes before you start

Although UpSignOn does handle the user password, we still recommand you keep allowing the standard login/password connection and the password forgotten procedure for your UpSignOn users for now.

Step by step guide

STEP 0 - Prerequisites

Partner account

You need a partner account so the application can check your licence.

To create your partner account, contact us at contact@upsignon.eu

You don't need a partner account for testing. Localhost domains (for instance localhost, 192.168.1.1) will be approved by the application.

User management system based on login/password

This doc assumes you already have a login/password and session system in place to handle your users connections.

STEP 1 - Link to UpSignOn

UpSignOn can be opened with an app link or universal link that contains two or three query parameters:

  • App Link upsignon://protocol/?url=<BASE_URL>&buttonId=<BUTTON_ID>
  • Universal Link https://upsignon.eu/protocol/?url=<BASE_URL>&buttonId=<BUTTON_ID>

Query parameters

urlurl of the UpSignOn API on your server (see STEP 2). Don't forget to use `encodeURIComponent` on the value.
buttonIdvalue that you define to distinguish between several buttons. [NB: This value can provide context about the user activity prior to opening the app. For example, your buttonId could be "event42" for the event number with id 42.]
connectionToken (optional)if a user is already logged-in, you can set this value to a unique connectionToken that identifies the user so UpSignOn can guide him into transforming his account into an account managed by UpSignOn. This token could be built like so: "userId.uuid". Of course this token should expire after a few minutes and should contain a random part. Think of it as a password forgotten mechanism. (This token will be sent back to you in the body of the /export-account request (see STEP 2) if the user validates it in his UpSignOn app.)

SECURITY NOTE to keep this token secret even to the user's browser extensions, the best practice would be to replace the button's link with a link to a route on your server that identifies the user with his session, generates the token, and returns a 303 redirection to upsignon://protocol/?url=BASE_URL&buttonId=IMPORT_BUTTON&connectionToken=token

NB: this link and the button's design can be integrated easily by using our JS library. Check https://github.com/UpSignOn/UpSignOn-front-helper-js/

Which link should you use?

When the user does not have the app installed on his device, he should be able to download it easily. Universal links solve this problem by opening the app instead of the website when the app is installed, but they are not supported on Windows and inside some mail clients. The app link on the other hand will fail if the user does not have the app installed on his device. So here is our recommandation about choosing which link to use.

Use caseLink typeComments
When you know that the user has the app on the current deviceApp link
QR codeUniversal linkQR codes will only be used on mobile endpoints where universal links are fully supported.
Web pageApp linkUniversal link would not work on windows. You should also display the link to https://upsignon.eu so users can download the app.
Native applicationApp linkInside a native application, you can detect if the app is installed and thus decide if you want to open the app (app link) or open the download page (https://upsignon.eu). See the annexe about this.
MailApp linkUniversal links will not work when opened by some mail clients.

You will typically have two buttons

A connect / create-account button

This button will be shown on your login page, when the user is not yet logged-in or does not yet have an account.

  • The /button-config route (see below) for this button will be configured with forceFormDisplay: false and disableAccountCreation: false
  • The link for this button will not contain a connectionToken parameter.

This way, if the user will have the opportunity to create an account via UpSignOn or to import his account into his UpSignOn environment if he already has one.

An export-account / update-data button

This button will be shown on the user's profile page, when the user is already logged-in. It could be named: "Update my data with UpSignOn".

  • The link for this button will be a link to a /redirect-to-export route (or whatever route name that suits you) on your server. This route will generate a connectionToken and return a 303 redirection to the UpSignOn App Link with the connectionToken (see the security warning in the explanation for the connectionToken).
  • The /button-config route (see below) for this button will be configured with forceFormDisplay: true and disableAccountCreation: true

This way, if the user already has his account in his UpSignOn environment, the button will invite him to update his data, but if he does not, the button will invite him to import his account into his UpSignOn environment.

STEP 2 - Add this API to your server

Note: All request bodies are json-formatted and all responses must also be sent as json

Note 2: It's surely the case in your database, but, just in case, note that this doc assumes your database uses a user id column separated from the email/login column. Using the login column as id could lead to issues for UpSignOn users who change their email address in the application.

Test your implementation with our tester module

0 - Define a BASE_URL

This url will hold the API endpoints specified in this section.

If your domain is domain.com, this url could be https://domain.com/upsignon-api for instance.

  • https is mandatory! UpSignOn will not communicate with insecure servers. Self-signed certificates will not work either.
  • subdomains are allowed
  • you can use any URL path you want, even no path (but this is not recommended for maintenance easiness)
  • the BASE_URL must not contain parameters or anchors"
  • the hostname will be displayed to the user, so choose it wisely

Note that for testing purposes, you can use localhost URLs http://localhost:PORT as your BASE_URL. The application will not check such requests and you won't have to pay. Using IP addresses in the following ranges will also work: [10.0.0.0 - 10.255.255.255], [172.16.0.0 - 172.31.255.255], [192.168.0.0 - 192.168.255.255].

Beware of conflicts with universal links! If you have an application associated to your website, read the /connect route documentation right now.

1 - Listen to /config

This route returns a general configuration. This controls how UpSignOn displays the user's account when opening the account details page in UpSignOn.

Receives

GET BASE_URL/config?lang=LANG

langLanguage used by the user, of the form fr or fr-BE

Returns 200

  • HEADERS
    • Content-Type: application/json
  • JSON body
    {
      "version": "1.0",
      "legalTerms": [
        {
          "id": "terms-of-use",
          "date": "2020-01-01",
          "link": "https://my-website.fr/fr/terms-of-use",
          "translatedText": "Terms of use"
        }
      ],
      "fields": [
        {
          "type": "firstname",
          "key": "firstname",
          "mandatory": true,
        },
        {
          "type": "postalAddress",
          "key": "deliveryAddress",
          "mandatory": true,
          "variant": "custom",
          "customLabel": "Delivery address"
        },
        {
          "type": "postalAddress",
          "key": "billingAddress",
          "mandatory": false,
          "variant": "custom",
          "customLabel": "Billing address",
          "maxSize": 1
        }
      ]
    }
    versionVersion of this config. Should be changed everytime this config changes. This has to be a string.
    legalTerms(OPTIONAL) List of terms the user needs to agree to in order to use your service
    legalTerms.idIdentifier of the document. Should be different for two different documents but should be the same between two versions of the document. This has to be a string. When a user agrees to your terms, the app remembers the agreement for the document identified by {id, date}. The link or the title of the document could change without needing to ask the user to agree to those terms again.
    legalTerms.dateDate the document was published ('YYYY-MM-DD'). This is used to decide if the app should present the document to the user
    legalTerms.linkThis url needs to return the document as an html page or pdf file. This page will be rendered as a webview in UpSignOn (html case). The document should be translated in the language of the user.
    legalTerms.translatedTextThe short description of the document that will be displayed to the user in the application.
    fieldsThis is the list of fields your app can request. The order of this list determines the order in which the fields are displayed in UpSignOn.
    fields.typeOne of the Requestable Fields values. (There can be several items with the same type in the list.) See below fro available options.
    fields.keyIdientifies the field uniquely. The variant or label could change, but this key is immutable. This key needs to be unique for each field.
    fields.variant[OPTIONAL] For each type of field, there exists a range of possible variants. Each standard variant will be displayed with a specific label in UpSignOn so you don't have to deal with translations. If none of the variants suit your need, you can define the type "custom" with a "customLabel" of your choice. (You can change this value at any time.) If this value is empty, it is equivalent to specifying a value of "default". Note that no standard variant has currently been added (coming soon). Send us an email at contact@upsignon.eu to request a new one.
    fields.customLabel[OPTIONAL] This value is used as the field label when "variant" is set to "custom".
    fields.mandatory[OPTIONAL] True to make this value always mandatory. User will not be allowed to empty it. If this is true, setting mandatory to false in the button config will have no effect. If this is false, setting mandatory to true in the button config will make the value mandatory for that case but user will be allowed to empty the value later.This should be true if the value is absolutely needed for the user account to be active.
    fields.maxSize[OPTIONAL] This value is only useful for fields which value is an array (such as postalAddress). It indicates the maximum number of value items that you accept.

Requestable Fields

This is the list of fields you can currently get from UpSignOn and their values. (Contact us if you would like us to add new fields).

Field typeValueComment
firstnamestring
lastnamestring
title'M' (male) | 'F' (female)
dateOfBirthYYYY-MM-DD
email
{
  address: string,
  isValidated: boolean,
}
phoneNumber
{
  number: string,
  isValidated: boolean,
}
uses the norm E.164 (starts with '+[CountryCode]', no spaces or special characters)
postalAddress
[
  {
    streetAddress: string,
    city: string,
    postalCode: string,
    country: string,
    otherInfo?: string
  }
]
the postalAddress field is an array of addresses. The streetAddress subfield can contain several lines using '\n'. otherInfo could contain the building security code for instance or can be empty.
iban
{
  IBAN: string,
  BIC: string|null,
  holderName: string|null
}
IBAN contains no spaces
newsletterConsent
{
  email: boolean,
  postal_mail: boolean
  phone: false,
  sms: boolean
}

2 - Listen to /button-config

This route maps each buttonId to its configuration. This controls how UpSignOn is opened when the user clicks on the corresponding button (fields requested, automatic connection vs forcing form display).

Receives

GET BASE_URL/button-config?buttonId=BUTTON_ID

buttonIdIdentifier of the link.

Returns a 404 if buttonId is not known to you.

Returns 200

  • HEADERS
    • Content-Type: application/json
  • JSON body
    {
      "fields": [
        { type: "firstname", key: "firstname", mandatory: true },
        { type: "postalAddress", key: "deliveryAddress", mandatory: true },
        { type: "postalAddress", key: "billingAddress", mandatory: false },
      ],
      "forceFormDisplay": false,
      "generalConfigVersion": "1.0",
      "disableAccountCreation": false
    }
    fieldsList of fields you need from the user in the context of this button. This list must be a subset of the list of fields in your general config.
    fields.typeOne of the Requestable Fields values
    fields.keyIdientifies the field uniquely.
    fields.mandatory[OPTIONAL] True to make the field mandatory in this context.
    generalConfigVersionVersion of the object returned by the "/config" route (this is used by the app to limit calls to the /config route on your server).
    forceFormDisplay[OPTIONAL] By default, when the user already has an account, UpSignOn will immediately connect and redirect the user to your website if you are not requesting any new field and if your configuration has not changed. Setting this value to true will force the app to show the form page so your users have the opportunity to update their data.
    disableAccountCreation[OPTIONAL] if true, prevents the account creation page from being displayed to the user. If the user does not have an account yet, only the import page will be displayed and the user will have to fill his login and password to import his account into his environment. Note that in addition to this mechanism, you can also set a `connectionToken` on the button's url query parameters when the user is already logged in. This will make the account import even simpler. See Step 1.

3.1 - Listen to /connect

This route is called in the process of connecting an existing user or just after an account creation.

Receives

POST BASE_URL/connect

{
  "userId": "e49f7d66-1326-4d13-a863-904e6cf7e612",
  "password": "Jtkr-wFtf-7CIp-hbPo",
  "buttonId": "SCOOTER_5455",
}

This route can be called with a null value for buttonId when the user connects to your website directly from UpSignOn.

Pseudo-code

  • if userId or password or buttonId is empty, return 401
  • check password
    • cryptographically-secure hash of password
    • get stored passwordHash for userId
    • if received and stored password hashes are different, return 401
  • generate a connectionToken
    • this token must be a UUID
    • expirationTime = now + 1 minute
  • Store (newUserId, connectionToken, expirationTime)
    (this token will be used and erased a few microseconds after its creation, so it might be enough to store it in memory rather than on disk)
  • map the buttonId to its redirectionUri (use a default redirectionUri when buttonId is empty)
  • return 200 with JSON content
    {
      "connectionToken": "11473772-106e-427a-949b-f3b938556f4d",
      "redirectionUri": "https://<BASE_URL>/redirection"
    }
    connectionTokena uuid that expires within 1 minute
    redirectionUri

    The uri to which the user will be redirected to. Can be an app link that opens your native app. (see below for details)

    PRO TIP: it can be useful to generate a buttonId dynamically and build the redirectionUri from it. For instance, buttonId could be "event42" and redirectionUri be "https://your-site.com/events/42/".

    SECURITY WARNING: make sure to always check that buttonId is indeed a valid and expected value before using it to generate the redirectionUri! Otherwise, you will create an Open Redirect breach.

3.2 - Listen to <redirectionUri>

This URI is returned by the connect route. You can have as many redirectionUris as you like but you need to follow this guide for each one. When UpSignOn receives the connectionToken, it redirects the user to this redirectionUri with 2 query parameters:

Beware of universal links if you have a website and a native app: If your app can be opened with universal links, make sure the redirectionUri does not match a link pattern that would open your app. Otherwise, your users might no longer be able to connect to your website as they would always be redirected to the app instead.

Receives

GET <redirectionUri>?userId=e49f7d66-...&connectionToken=11473772-...

userIdthe user identifier
connectionTokenthe token just received

Pseudo-code

  • check the connectionToken is indeed associated with this userId
  • check the connectionToken has not expired
  • if one of these tests fails
    • return an HTML page requesting the user to immediately update his password from UpSignOn (that should be very rare though, so no need for a fancy page)
  • Delete the connectionToken so it can only be used once

    This is critical for the user's security! A connectionToken can only be used once to avoid replay attacks! Not complying could leak to data leak and engage your responsibility.

  • create a session cookie (or whatever mecanism you use to keep the user logged in)
  • return the html content to display to the user along with the session cookie

    You could also return an http redirect so the user is redirected to a user-friendly url where he cannot see the connectionToken

If you want to redirect the user to your native app, you can use a universal link or an app link. Your app will have to make a call to your server with the userId and connectionToken to check the connectionToken as above and authenticate the user. To learn how to enable universal links or app links in your native app, see the resources in annex.

In case you decide to have a <BASE_URL>/redirect route that does this check and then returns an http redirect to the user, beware of open redirect security flaws.

4 - Listen to /create-account

This route creates a user account on your server.

UpSignOn will wait for the explicit consent of the user to send this request.
Receiving this request implies that the user has agreed to your terms specified in the "/config" route.

Receives

POST BASE_URL/create-account

passwordA strong password generated automatically by UpSignOn
data(OPTIONAL) array of objects { type, key, value } with the values that were requested with the button (see "/button-config"). Can be the empty array []. If a value is not mandatory and the user did not fill it, it might not be added to this array.

Pseudo-code

  • if password is empty, return 400
  • cryptographically-secure hash of password
  • generate new user id
  • permanent storage of (newUserId, passwordHash, [data]).
  • return 200 with JSON content
    {
      "userId": uniqueId,
    }
  • return 403 if you refuse to create the account (for instance if the user is too young)
    { "message": "Explanation for refusal"}

5 - Listen to /export-account

This route "exports" an existing account and its data so the user can import it in his environment.

Receives

POST BASE_URL/export-account

This route can receive 2 sets of parameters:
currentLoginlogin of an existing account
currentPasswordpassword of the same existing account
newPassworda strong password generated automatically by UpSignOn or the same value as currentPassword
or
connectionTokenthe token that was passed into the UpSignOn link (see STEP 1)
newPassworda strong password generated automatically by UpSignOn

Pseudo-code

  • if newPassword is empty, return 400
  • if connectionToken and (currentLogin or currentPassword) are empty, return 400
  • if connectionToken and currentLogin and currentPassword are not empty, return 400
  • CASE 1: the body contains currentPassword & currentLogin
    • if currentLogin does not match a valid login in your database, return 401
    • check currentPassword vs currentLogin
    • if account does not exist or if the password mismatches, return 401
  • CASE 2: the body contains a connectionToken
    • check the connectionToken validity and get the associated user
    • if the token is not valid, return 401
    • if the token is valid, invalidate it so it cannot be reused. Then continue.
  • Replace the account's password with the newPassword (stored as hash of course)
  • Retrieve all user data and fill the response content with it so the user does not have to fill your form again
    {
      // Response content
      "userId": userId,
      "userData?": [
        // Array of { type, key, value } objects.
        // Add all values that you know of. Use the same field types and keys as as those described in your /config route.
        // The `value` field must be of the same type as the one described in the Requestable Fields (see /config section).
        // Invalid values will be ignored, except in the following cases
        //   - missing country in a postalAddress
        //   - wrong number format in a phoneNumber
        {
          type: "firstname",
          key: "firstname",
          value: "John"
        },
        {
          type: "postalAddress",
          key: "deliveryAddress",
          value: [
            { streetAddress: string, city: string, postalCode: string, country: string, otherInfo?: string }
          ]
        }
      ]
    }
  • return 200 with above JSON content

6 - Listen to /update-data

This method is called when the user updates one of his information to which you had access.

Receives

POST BASE_URL/update-data

userIduser identifier
passworduser password
dataarray of objects { type, key, value } with all the values to update. Will not be null or empty. If a user chooses to stop sharing an optional value with you, `value` will be null.

Pseudo-code

  • if userId or data is empty, return 400
  • if password is empty, return 401
  • if account does not exist, return 401
  • check password
    • cryptographically-secure hash of password
    • get stored passwordHash for userId
    • if received and stored password hashes are different, return 401
  • If you cannot update the user data because some requirement is not met (for instance if you only accept email addresses from a certain domain and the email address that was sent is not in that domain), return a 403 with a message body to explain the problem. In that case, do not update the other data as well because UpSignOn will cancel the whole update in the user's environment.
    { "message": "Explanation for refusal"}
  • update stored user data

    This route is called when a user demands a change for at least one of his information and you already had access to the previous version of this information. Except for legal reasons, you must not keep a copy of the previous data !

    A null value in the data object means that the user has stopped sharing the info with you. You must thus delete it whenever possible.

  • return 200

If for any reason, your server does not return a 200, the request will be retried later.

7 - Listen to /update-password

Receives

POST BASE_URL/update-password

{
  "userId": string,
  "password": string,
  "newPassword": string
}

Pseudo-code

  • securely parse request body
  • if userId is empty, return 400
  • if newPassword is empty, return 400
  • if password is empty, return 401
  • if account does not exist, return 401
  • check password
    • cryptographically-secure hash of password
    • get stored passwordHash for userId
    • if received and stored password hashes are equal
      • replace password hash with newPassword hash
      • return 200
      • or return 403 if you cannot update the user password yet or if the password does not meet your security requirements (the application does offer the user the opportunity to define his own password instead of using a random one).
        { "message": "Explanation for refusal"}
    • if received and stored password hashes differ, check newPassword:
      • cryptographically-secure hash of newPassword
      • if newPassword hash and stored password hash are equal, do nothing but return 200 (this scenario is important in case the user has updated his password manually from your site and wants to reflect the change in his UpSignOn environment).
      • otherwise if they don't match either, return 401

8 - Listen to /delete-account-and-data

This needs to be treated as an official request from the user to have his account and data deleted.

Receives

POST BASE_URL/delete-account-and-data

{
  "userId": string,
  "password": string,
}

Pseudo-code

  • securely parse request body
  • if userId is empty, return 400
  • if password is empty, return 401
  • if account does not exist, return 200 with JSON content
    { deletionStatus: "DONE" }
  • check password
    • cryptographically-secure hash of password
    • get stored passwordHash for userId
    • if received and stored password hashes are different, return 401
  • If you can delete all user data right now, do it and return 200 with JSON content
    { deletionStatus: "DONE" }
  • If you have legitimate reasons not to delete user data, return 200 with JSON content
    { deletionStatus: "DENIED" }
  • If you can't or don't want to delete user data right now but will do it within 30 days (the maximum legal time in Europe), return 200 with JSON content
    { deletionStatus: "PENDING"}
    This means you have taken the user's request into account and will take the necessary actions to make it happen. In this scenario, the account will not be deleted from the user's app yet. You may still receive data updates, but you must not consider such updates as a cancelation of the deletion request.

    If you return "PENDING", you have to listen to route /get-account-deletion-status!

9 - Listen to /get-account-deletion-status

This route only needs to be implemented when you return "PENDING" in /delete-account-and-data.

Receives

POST BASE_URL/get-account-deletion-status

{
  "userId": string,
  "password": string,
}

Pseudo-code

  • securely parse request body
  • if userId is empty, return 400
  • if password is empty, return 401
  • if account does not exist, return 200 with JSON content
    { deletionStatus: "DONE" }
  • check password
    • cryptographically-secure hash of password
    • get stored passwordHash for userId
    • if received and stored password hashes are different, return 401
  • if you still need time to delete the account, return 200 with JSON content
    { deletionStatus: "PENDING" }
  • if you got the user to cancel his deletion request, return 200 with JSON content
    { deletionStatus: "CANCELED" }
Test your implementation with our tester module

STEP 3 - Adapt your standard login procedure

Connection via standard form

The user should be able to connect to his account via your usual login/password form but using the userId (which appears as login in UpSignOn) and password copied from UpSignOn.
This is important in the case when the user cannot have UpSignOn on some device and is ready to manually copy his credentials from UpSignOn.

Account recovery

The user should also be able to recover his account using your forgotten password procedure by knowing only his email address or phone number or name (or other identifying information).
This is important in the case when the user looses access to his UpSignOn environment or chooses to delete his account only in UpSignOn (while not deleting his account on your servers).

STEP 4 - Security check

UpSignOn disclaims all responsability for any security breach in your implementation.

All these routes are public APIs, so they must be secure against attackers.

Double check that

  • after each successfull call to your redirectionUris, the connectionToken is invalidated
  • after each account export using a connectionToken, that connectionToken is invalidated
  • your /connect route does not create an Open Redirect breach: this could happen if your returned redirectionUri includes the buttonId as is, with no check. This could also happen if your redirectionUri does itself return a redirection status using some unchecked input parameter.

Also check general good security practices

  • data sanitization
    • check for tags in strings,
    • do not use object methods on untrusted objects
  • prepared statements to prevent SQL injections,
  • prevention of regex denial of service attack (if you use regexes in these routes)
  • all other security measures associated to your context

Other considerations

  • you should make sure the connection tokens cannot be backed up. If you need to roll back to a previous state of your database, you don't want anyone to be able to replay those connection tokens.
  • you should periodically clean the unused old tokens from your database. (Such tokens could remain unused in rare cases when the user suddenly looses internet connection between two requests) or if the user does not finish the export step.

STEP 5 - Scenarii to test

Once your routes are implemented, here is what you should try to do with the app:

  1. Test all your buttons open the application without showing an error while you still don't have an UpSigner account in UpSignOn for your website / app.
  2. Test account creation
  3. Test account import to UpSignOn
  4. Check your main connection button does not forceFormDisplay to allow for a smooth, automatic connection
  5. Test other buttons allow you to edit what information you share and check that these editions are taken into account in your database
  6. Now, open UpSignOn without using a button and test that clicking on your website / app in the list does open your website / app
  7. In UpSignOn, open the details page for your website / app, and check you can edit the data
  8. then check you can renew the password and still be able to connect to your website / app
  9. check you can connect to your account with the standard login/password form using the userId and password from UpSignOn and using the usual login (email for instance) with the password from UpSignOn
  10. check your are still able to renew your password by using the standard forgotten password procedure on your website / app without using the userId stored in UpSignOn
  11. then test account deletion
You can also test your implementation with our tester module

ANNEX 1 - Working with native apps

Usefull links for app link support on various platforms

Detecting if UpSignOn is installed on the device

  • React-Native: see Linking.canOpenUrl api
  • iOS: see the official documentation

    On iOS, in order to detect if UpSignOn is installed, you will have to add an entry LSApplicationQueriesSchemes to your info.plist and set its value to an array containing the value upsignon.

  • Android: if the resolveActivity method of an intent returns null, then the app link cannot be opened.

ANNEX 2 - Common issues

One of the fields you want to get is not displayed to the user

  • Make sure the /button-config result contains this field
  • Make sure your /config result contains this field
  • Make sure the 'type' key of the field's description is correctly spelled in /button-config and in /config and is one of the authorized field types.