Daedaluz / uyulala

Web based authenticator, implementing OAuth2 code flow and BankID-like api interface.
MIT License
1 stars 0 forks source link
oauth2-server passkeys webauthn

Uyulala

Description

Uyulala at it's core is a web based authenticator that only uses passkeys as a means of authentication. Implementing a Bank-ID similar api and a OAuth2 interface, enables uyulala to be used as a IDP for other applications. It is a simple and secure way to authenticate users without the need for usernames or passwords.

Features

Future plans

Running a local test server

$ docker compose up -d --build
$ docker exec -ti uyulala uyulala create key
$ docker exec -ti uyulala uyulala create app --demo demo

Point a browser at https://localhost/demo

Test use case

1) Protect a grafana instance with uyulala via custom oauth2 settings.

    $ docker compose -f docker-compose.usecase.yml up -d --build
    $ # wait a little for uyulala to install the database and run migrations (and load metadata)
    $ docker exec -ti uyulala uyulala create key
    $ docker exec -ti uyulala uyulala create app --demo demo

2) Create a user with a registered key by going to http://localhost/demo 3) point browser to http://localhost:3000/ 4) Authenticate with key 5) ???? 6) Profit

BankID flow

sequenceDiagram
  ClientFrontend->>ClientServer: Login
  ClientServer->>Uyulala: /api/v1/sign
  Uyulala->>ClientServer: {"challengeId":"xxx"}
  ClientServer->>ClientFrontend: Present Link / Qr
  ClientServer-->>Uyulala: /api/v1/collect
  Note over ClientServer,Uyulala: Repeat till success or rejected/expired
  User->>Uyulala:  Signs / Reject
  ClientServer->>Uyulala: /api/v1/collect
  Uyulala->>ClientServer: {"status": "success", ...}
  ClientServer->>ClientFrontend: User Loged in

API

The API is split into four parts;


Client API

Client authentication is done with Basic authentication using client id as username and client secret as password. If OAuth2 is used, client id and client secret can be sent both as form data and basic auth.


POST /api/v1/collect

curl -u "demo:demo" \
     -H 'Content-Type: application/json' \
     -d '{"challengeId":"challenge-id"}' \
     http://localhost:8080/api/v1/collect

This endpoint is similar to the Bank-ID collect in the same sense that it is used to collect a challenge and that the client should poll this endpoint until it either expires or the user signs the challenge. This endpoint also doubles as the OAuth2 token endpoint if the Content-Type header is application/x-www-form-urlencoded.

Using OAuth2, the code is generated when the user has signed the challenge and the challenge is started with an oauth2 flow.

Example request payload

{
  "challengeId": "12ca6a2e-f783-4545-92f2-4d80cb74de45"
}

Example signed result:

{
  "challengeId": "12ca6a2e-f783-4545-92f2-4d80cb74de45",
  "userId": "ABCDEFG",
  "status": "signed",
  "signed": "1970-01-01T00:00:00Z",
  "userPresent": true,
  "userVerified": true,
  "publicKey": "",
  "assertionResponse": {
    "clientDataJSON": "",
    "authenticatorData": "",
    "signature": "abasc",
    "userHandle": "abcdef"
  },
  "challenge": "",
  "signatureData": {
    "text": "",
    "data": ""
  }
}

Example pending result (Not yet viewed):

{
  "msg": "Challenge has not been signed yet",
  "status": "pending"
}

Example viewed result:

{
  "msg": "Challenge has not been signed yet",
  "status": "viewed"
}

Example rejected result:

{
  "msg": "Challenge has been rejected",
  "status": "rejected"
}

Example collected result

{
  "msg": "Challenge has already been collected",
  "status": "collected"
}

Example expired result:

{
  "msg": "Challenge has expired",
  "status": "expired"
}

POST /api/v1/sign

This api is used to create a challenge for the user to sign, also inspired by the Bank-ID api. The application needs to redirect the user to the authenticator page with the challenge id as a query parameter eg http://localhost:8080/authenticator?challengeId=12ca6a2e-f783-4545-92f2-4d80cb74de45

Example request payload:

{
  "userId": "ABCDEFG",
  "userVerification": "required",
  "text": "",
  "data": "",
  "timeout": 300,
  "redirect": "https://example.com/authenticated"
}

Example response payload:

{
  "challenge_id": "cbe4748d-2c98-434f-8e72-d32fbbdc86b8"
}

Public API

The public api is used by the web ui and has no authentication.


GET /api/v1/challenge/:id

This api is used to get the challenge data for a specific challenge id.

curl -H 'Content-Type: application/json' \
     http://localhost:8080/api/v1/challenge/12ca6a2e-f783-4545-92f2-4d80cb74de45

example response payload for signing:

{
  "app": {
    "id": "nfh17afcbd1e6add1d1d",
    "name": "demo",
    "created": "2023-11-15T08:25:56Z",
    "description": "",
    "icon": "",
    "idTokenAlg": "RS256",
    "keyId": "asdavafafadqd",
    "admin": false
  },
  "expire": 1700146828,
  "publicKey": {
    "challenge": "<some challenge>",
    "timeout": 300000,
    "rpId": "localhost",
    "allowCredentials": [
      {
        "type": "public-key",
        "id": ""
      },
      {
        "type": "public-key",
        "id": ""
      }
    ],
    "userVerification": "required"
  },
  "type": "webauthn.get"
}

example response payload for creating a key:

{
  "app": {
    "id": "nfh17afcbd1e6add1d1d",
    "name": "demo",
    "created": "2023-11-15T08:25:56Z",
    "description": "",
    "icon": "",
    "idTokenAlg": "RS256",
    "keyId": "aaaadbbasdasd",
    "admin": true
  },
  "expire": 1700087418,
  "publicKey": {
    "rp": {
      "name": "uyulala",
      "id": "localhost"
    },
    "user": {
      "name": "Kalle Anka",
      "displayName": "Kalle Anka",
      "id": "ABCDEFG"
    },
    "challenge": "",
    "pubKeyCredParams": [
      {
        "type": "public-key",
        "alg": -7
      },
      {
        "type": "public-key",
        "alg": -35
      },
      {
        "type": "public-key",
        "alg": -36
      },
      {
        "type": "public-key",
        "alg": -257
      },
      {
        "type": "public-key",
        "alg": -258
      },
      {
        "type": "public-key",
        "alg": -259
      },
      {
        "type": "public-key",
        "alg": -37
      },
      {
        "type": "public-key",
        "alg": -38
      },
      {
        "type": "public-key",
        "alg": -39
      },
      {
        "type": "public-key",
        "alg": -8
      }
    ],
    "timeout": 300000,
    "authenticatorSelection": {
      "authenticatorAttachment": "cross-platform",
      "requireResidentKey": true,
      "residentKey": "required",
      "userVerification": "required"
    },
    "attestation": "direct"
  },
  "type": "webauthn.create"
}

POST /api/v1/challenge/:id

This api is used to sign a challenge.

example post data:

{
  "response": {
    "clientDataJSON": "...",
    "authenticatorData": "...",
    "signature": "...",
    "userHandle": "..."
  },
  "rawId": "...",
  "authenticatorAttachment": "cross-platform",
  "type": "public-key",
  "id": "...."
}

example response payload:

{
  "redirect": "http://localhost:8080/demo?challengeId=12ca6a2e-f783-4545-92f2-4d80cb74de45"
}

Service API

The service api is used to create / delete users and add / remove keys respectively. Service authentication is same as with the client api, but the client needs the admin flag set during creation.


GET /api/v1/service/list/users

curl -u "demo:demo" \
     -H 'Content-Type: application/json' \
     http://localhost:8080/api/v1/service/list/users

example response payload:

[
  {
    "id": "ea85972bed2a603fb4480ff6980fd530a846",
    "created": "2023-11-15T08:31:02Z",
    "keys": [
      {
        "hash": "<sha hash of key>",
        "key": {
          "ID": "<key id>",
          "PublicKey": "<public key>",
          "AttestationType": "packed",
          "Transport": null,
          "Flags": {
            "UserPresent": true,
            "UserVerified": true,
            "BackupEligible": false,
            "BackupState": false
          },
          "Authenticator": {
            "AAGUID": "<some AAGUID>",
            "SignCount": 14,
            "CloneWarning": false,
            "Attachment": "cross-platform"
          }
        },
        "created": "2023-11-15T08:31:11Z",
        "lastUsed": "2023-11-15T21:31:09Z"
      },
      {
        "hash": "<sha hash of key>",
        "key": {
          "ID": "",
          "PublicKey": "",
          "AttestationType": "none",
          "Transport": null,
          "Flags": {
            "UserPresent": true,
            "UserVerified": true,
            "BackupEligible": true,
            "BackupState": true
          },
          "Authenticator": {
            "AAGUID": "",
            "SignCount": 0,
            "CloneWarning": false,
            "Attachment": "cross-platform"
          }
        },
        "created": "2023-11-15T09:44:09Z",
        "lastUsed": "2023-11-15T09:47:11Z"
      }
    ]
  }
]

POST /api/v1/service/create/user

This api creates a new user and returns a new challenge id that creates the users first key when signed. Like the sign api, the application needs to redirect the user to the authenticator page with the challenge id as a query parameter.

curl -u "demo:demo" \
     -H 'Content-Type: application/json' \
     -d '{"suggestedName": "Kalle Anka", "timeout": 380, "redirect": "http://localhost:8080/demo"}' \
     http://localhost:8080/api/v1/service/create/user

example request payload:

{
  "suggestedName": "Kalle Anka",
  "timeout": 380,
  "redirect": "http://localhost:8080/demo"
}

example response payload:

{
  "challengeId": "12ca6a2e-f783-4545-92f2-4d80cb74de45"
}

POST /api/v1/service/create/key

This api creates a new key for the user and returns a new challenge id that creates the key associated with the user when signed. Like the sign api, the application needs to redirect the user to the authenticator page with the challenge id as a query parameter.

curl -u "demo:demo" \
     -H 'Content-Type: application/json' \
     -d '{"userId": "ABCDEFG", "timeout": 380, "redirect": "http://localhost:8080/demo", "suggestedName": "Kalle Anka"}' \
     http://localhost:8080/api/v1/service/create/key

example request payload:

{
  "userId": "ABCDEFG",
  "timeout": 380,
  "redirect": "http://localhost:8080/demo",
  "suggestedName": "Kalle Anka"
}

example response payload:

{
  "challengeId": "12ca6a2e-f783-4545-92f2-4d80cb74de45"
}

POST /api/v1/service/delete/key

This api deletes a key for the user.

curl -u "demo:demo" \
     -H 'Content-Type: application/json' \
     -d '{"userId": "ABCDEFG", "keyHash": "<key id sha hash>"}' \
     http://localhost:8080/api/v1/service/delete/key

example request payload:

{
  "userId": "ABCDEFG",
  "keyHash": "<key id sha hash>"
}

example response payload:

{
  "status": "deleted"
}

POST /api/v1/service/delete/user

This api deletes a user and all associated keys.

curl -u "demo:demo" \
     -H 'Content-Type: application/json' \
     -d '{"userId": "ABCDEFG"}' \
     http://localhost:8080/api/v1/service/delete/user

example request payload:

{
  "userId": "ABCDEFG"
}

example response payload:

{
  "status": "deleted"
}