Fly with Passkey × AWS Cognito = Passquito!
A PoC on passkey authentication inspired by aws-samples/amazon-cognito-passwordless-auth
.
Features:
- Rust × AWS Lambda → Snappy cold start!
- AWS Cognito Lambda triggers
- 💩 Ugly codebase
-
A user signs up to your app.
-
Your app starts a registration session by sending a request to the registration start endpoint (
POST /registration/start
). -
Your app requests the user to create a passkey.
-
The user creates a passkey and signs the challenge associated with the registration session → the signature.
-
The user provides your app with a public key credential which includes the public key of the passkey and the signature.
-
Your app finishes the registration session by sending a request to the registration finish endpoint (
POST /registration/finish
). -
The registration finish endpoint verifies the public key credential.
-
The registration finish endpoint stores the public key in the credential store for future authentication of the user.
Sequence diagram:
sequenceDiagram
actor User
participant YourApp
participant RegistrationStartEndpoint
participant RegistrationFinishEndpoint
participant CredentialStore
User-)YourApp: Sign up
YourApp-)RegistrationStartEndpoint: POST /registration/start
YourApp-)User: Request to create a passkey
User-)User: Create a passkey and sign the challenge
User-)YourApp: Public key credential
YourApp-)RegistrationFinishEndpoint: POST /registration/finish
RegistrationFinishEndpoint-)RegistrationFinishEndpoint: Verifies the public key credential
RegistrationFinishEndpoint-)CredentialStore: Store the public key of the passkey
-
A user opens your app.
-
Your app starts a discoverable authentication session by sending a request to the discoverable endpoint (
POST /authentication/discover
). -
Your app requests the user to select a passkey that is associated with your app.
-
The user selects a passkey and signs the challenge associated with the discoverable authentication session → the signature.
-
The user provides your app with a public key credential which includes the key ID of the passkey and the signature.
-
Your app initiates a Cognito authentication session by sending a request to the authentication start endpoint (
POST /authentication/start
) but ignores the challenge returned from this endpoint. -
Your app finishes the Cognito authentication session by sending the challenge and the public key credential to the authentication finish endpoint (
POST /authentication/finish
). -
The authentication finish endpoint restores the public key associated with the key ID from the credential store.
-
The authentication finish endpoint verifies the public key credential.
-
The authentication finish endpoint issues Cognito tokens to your app.
Sequence diagram:
sequenceDiagram
actor User
participant YourApp
participant DiscoverableEndpoint
participant AuthenticationStartEndpoint
participant AuthenticationFinishEndpoint
participant CredentialStore
User-)YourApp: Open
YourApp-)DiscoverableEndpoint: POST /authentication/discover
YourApp-)User: Request to select a passkey
User-)User: Select a passkey and sign the challenge
User-)YourApp: Public key credential
YourApp-)AuthenticationStartEndpoint: POST /authentication/start
YourApp-)AuthenticationFinishEndpoint: POST /authentication/finish
AuthenticationFinishEndpoint-)CredentialStore: Restore the public key
AuthenticationFinishEndpoint-)AuthenticationFinishEndpoint: Verifies the public key credential
AuthenticationFinishEndpoint-)YourApp: Cognito tokens
The usage scenarios consist of two major parts:
The registration scenarios have two variations:
The following sections in Web Authentication: An API for accessing Public Key Credentials Level 3 are recommended to read for better understanding of the scenarios:
-
Your app provides a form for a user to sign up.
-
The user fills the form with the username and the display name.
Neither the username nor the display name are necessarily unique. They are provided for the user to locate the passkey in user's device.
-
The user hits the sign up button.
-
Your app POSTs the username and the display name to the registration start endpoint (
/registration/start
). -
The registration start endpoint generates a unique ID for the user → the user ID.
-
The registration start endpoint creates a public key credential creation options which includes a challenge.
The user handle of the public key credential creation options is equal to the user ID.
-
The registration start endpoint stores a new registration session in the session store.
The registration session includes the following parameters:
- session ID: the primary key
- user ID
- username
- display name
- challenge
-
The registration start endpoint returns the session ID and public key credential creation options to your app.
-
Your app initiates public key creation with the public key credential creation options.
-
The user authorizes the public key creation.
-
User's authenticator creates a new key pair (a private key and a public key).
-
User's authenticator signs the challenge with the private key → the signature.
-
User's authenticator returns a public key credential which includes the public key and the signature to your app.
-
Your app POSTs the session ID and the public key credential to the registration finish endpoint (
/registration/finish
). -
The registration finish endpoint pops the registration session associated with the session ID from the session store.
-
The registration finish endpoint verifies the public key credential.
The following parameters are involved in the verification:
- challenge
- public key
- signature
-
The registration finish endpoint creates a new Cognito user with the following attributes:
username
: user IDpreferred_username
: usernamename
: display name
The Cognito user is provided with a random password, which is confirmed upon creation; i.e., the user never faces it.
-
The registration finish endpoint stores the public key along with the following parameters in the credential store:
- user ID: the primary key
- The credential ID of the public key: the primary key
- The sub attribute of the Cognito user
-
The registration finish endpoint returns an empty OK response to your app.
Sequence diagram:
sequenceDiagram
actor User
participant Authenticator
participant YourApp
participant RegistrationStartEndpoint
participant RegistrationFinishEndpoint
participant SessionStore
participant CredentialStore
participant Cognito
YourApp-)User: Provide the form
User-)YourApp: Fill the form
User-)YourApp: Hit the sign up button
activate YourApp
YourApp->>RegistrationStartEndpoint: POST /registration/start
activate RegistrationStartEndpoint
RegistrationStartEndpoint->>RegistrationStartEndpoint: Generate a user ID
RegistrationStartEndpoint->>RegistrationStartEndpoint: Create a public key credential creation options
RegistrationStartEndpoint->>SessionStore: Store a registration session
activate SessionStore
SessionStore-->>RegistrationStartEndpoint: Registration session
deactivate SessionStore
RegistrationStartEndpoint-->>YourApp: Session ID, public key credential creation options
deactivate RegistrationStartEndpoint
YourApp-)Authenticator: Initiate a public key creation
activate Authenticator
deactivate YourApp
Authenticator-)User: Ask for the authorization
User-)Authenticator: Authorize the public key creation
Authenticator->>Authenticator: Create a new key pair
Authenticator->>Authenticator: Sign the challenge
Authenticator-)YourApp: Public key credential
activate YourApp
deactivate Authenticator
YourApp->>RegistrationFinishEndpoint: POST /registration/finish
activate RegistrationFinishEndpoint
RegistrationFinishEndpoint->>SessionStore: Pop the registration session
activate SessionStore
SessionStore-->>RegistrationFinishEndpoint: Registration session
deactivate SessionStore
RegistrationFinishEndpoint->>RegistrationFinishEndpoint: Verify the public key credential
RegistrationFinishEndpoint->>Cognito: Create a new Cognito user
activate Cognito
Cognito-->>RegistrationFinishEndpoint: Cognito user
deactivate Cognito
RegistrationFinishEndpoint->>CredentialStore: Store the public key
activate CredentialStore
CredentialStore-->>RegistrationFinishEndpoint: OK
deactivate CredentialStore
RegistrationFinishEndpoint-->>YourApp: OK
deactivate RegistrationFinishEndpoint
YourApp-)User: OK
deactivate YourApp
This scenario supposes that the user has a valid passkey in another device.
-
A user opens your app in a new device.
-
The user signs in to your app with a passkey in another device through cross-device authentication, and receives an ID token.
See Section Authentication scenarios for the details of authentication.
-
Your app shows a form for the user to register a new device.
-
The user fills the form with the username and the display name.
Neither the username nor the display name are necessarily unique. They are provided for the user to locate the passkey in the new device.
-
The user hits the register button.
-
Your app POSTs the username and the display name to the verified registration start endpoint (
/registration/start-verified
). -
The verified registration start endpoint authenticates the user with the ID token.
-
The verified registration start endpoint derives the user ID from the ID token.
(The following steps are equivalent to the steps in Scenario Registration of a new user.)
-
The verified registration start endpoint creates a public key credential creation options which includes a challenge.
The user handle of the public key credential creation options is equal to the user ID.
-
The verified registration start endpoint stores a new registration session in the session store.
The registration session includes the following parameters:
- session ID: the primary key
- user ID
- username
- display name
- challenge
-
The verified registration start endpoint returns the session ID and public key credential creation options to your app.
-
Your app initiates public key creation with the public key credential creation options.
-
The user authorizes the public key creation.
-
User's authenticator in the new device creates a new key pair (a private key and a public key).
-
User's authenticator in the new device signs the challenge with the private key → the signature.
-
User's authenticator in the new device returns a public key credential which includes the public key and the signature to your app.
-
Your app POSTs the session ID and the public key credential to the resigration finish endpoint (
/registration/finish
). -
The registration finish endpoint pops the registration session associated with the session ID from the session store.
-
The registration finish endpoint verifies the public key credential.
The following parameters are involved in the verification:
- challenge
- public key
- signature
-
The registration finish endpoint creates a new Cognito user with the following attributes:
username
: user IDpreferred_username
: usernamename
: display name
The Cognito user is provided with a random password, which is confirmed upon creation; i.e., the user never faces it.
-
The registration finish endpoint stores the public key along with the following parameters in the credential store:
- user ID: the primary key
- The credential ID of the public key: the primary key
- The sub attribute of the Cognito user
-
The registration finish endpoint returns an empty OK response to your app.
Sequence diagram:
sequenceDiagram
participant User
participant Authenticator
participant YourApp
participant VerifiedRegistrationStartEndpoint
participant RegistrationFinishEndpoint
participant SessionStore
participant CredentialStore
participant Cognito
User-)YourApp: Sign with another device (cross-device authentication)
YourApp-)User: Provide the form
User-)YourApp: Fill the form
User-)YourApp: Hit the register button
activate YourApp
YourApp->>VerifiedRegistrationStartEndpoint: POST /registration/start-verified
activate VerifiedRegistrationStartEndpoint
VerifiedRegistrationStartEndpoint->>VerifiedRegistrationStartEndpoint: Derive the user ID from the ID token
VerifiedRegistrationStartEndpoint->>VerifiedRegistrationStartEndpoint: Create a public key credential creation options
VerifiedRegistrationStartEndpoint->>SessionStore: Store a registration session
activate SessionStore
SessionStore-->>VerifiedRegistrationStartEndpoint: Registration session
deactivate SessionStore
VerifiedRegistrationStartEndpoint-->>YourApp: Session ID, public key credential creation options
deactivate VerifiedRegistrationStartEndpoint
YourApp-)Authenticator: Initiate a public key creation
activate Authenticator
deactivate YourApp
Authenticator-)User: Ask for the authorization
User-)Authenticator: Authorize the public key creation
Authenticator->>Authenticator: Create a new key pair
Authenticator->>Authenticator: Sign the challenge
Authenticator-)YourApp: Public key credential
activate YourApp
deactivate Authenticator
YourApp->>RegistrationFinishEndpoint: POST /registration/finish
activate RegistrationFinishEndpoint
RegistrationFinishEndpoint->>SessionStore: Pop the registration session
activate SessionStore
SessionStore-->>RegistrationFinishEndpoint: Registration session
deactivate SessionStore
RegistrationFinishEndpoint->>RegistrationFinishEndpoint: Verify the public key credential
RegistrationFinishEndpoint->>Cognito: Create a new Cognito user
activate Cognito
Cognito-->>RegistrationFinishEndpoint: Cognito user
deactivate Cognito
RegistrationFinishEndpoint->>CredentialStore: Store the public key
activate CredentialStore
CredentialStore-->>RegistrationFinishEndpoint: OK
deactivate CredentialStore
RegistrationFinishEndpoint-->>YourApp: OK
deactivate RegistrationFinishEndpoint
YourApp-)User: OK
deactivate YourApp
The authentication scenarios have two variations:
The authentication sceanrios utilize the custom authentication challenge Lambda triggers of AWS Cognito to implement a custom authentication flow. Please refer to Section Custom authentication challenge Lambda triggers in Amazon Cognito Developer Guide for better understanding of the custom authentication flow.
-
Your app shows a web page for a user to sign in.
-
Your app sends a POST request to the discoverable endpoint (
/authentication/discover
). -
The discoverable endpoint creates a public key credential request options which include a challenge and a relying party ID.
-
The discoverable endpoint stores a new discoverable authentication session in the session store.
The discoverable authentication session includes the following parameters:
- challenge: the primary key
- public key credential request options
-
The discoverable endpoint returns the public key credential request options to your app.
-
Your app initiates a discoverable credential request with the public key credential request options.
-
User's authenticator asks the user to select a passkey from those associated with the relying party ID in user's authenticator.
A passkey includes a key pair of a private key and a public key.
-
The user selects a passkey.
-
User's authenticator asks the user to authorize the use of the passkey.
-
The user authorizes the use of the passkey; e.g., using biometrics.
-
User's authenticator signs the challenge with the private key → the signature.
-
User's authenticator returns a public key credential which includes the public key and the signature to your app.
-
Your app extracts the user handle from the public key credential, which is equal to the user ID.
-
Your app POSTs the user ID to the authentication start endpoint (
/authentication/start
). -
The authentication start endpoint calls the InitiateAuth AWS Cognito API with the following parameters:
AuthFlow
:"CUSTOM_AUTH"
AuthParameters
:USERNAME
: user ID
-
AWS Cognito invokes the define auth challenge trigger.
-
The define auth challenge trigger initiates a custom authentication flow.
-
AWS Cognito invokes the create auth challenge trigger.
-
The create auth challenge trigger returns a dummy challenge parameter.
The true challenge was created at Step 3.
-
AWS Cognito returns a session ID and public key credential request options to the authentication start endpoint.
-
The authentication start endpoint returns the session ID and the public key credential request options to your app.
The public key credential request options returned here are never used.
-
Your app POSTs the following parameters to the authentication finish endpoint (
/authentication/finish
):- session ID
- public key credential
- user ID
-
The authentication finish endpoint calls the RespondToAuthChallenge AWS Cognito API with the following parameters:
ChallengeName
:"CUSTOM_CHALLENGE"
Session
: session IDChallengeResponses
:USERNAME
: user IDANSWER
: public key credential
-
AWS Cognito invokes the verify auth challenge trigger.
-
The verify auth challenge trigger pops the discoverable authentication session associated with the challenge from the session store.
This is the discoverable authentication session stored at Step 4.
-
The verify auth challenge trigger queries the credential store for the public keys associated with the user ID.
-
The verify auth challenge trigger verifies the public key credential.
The following parameters are involved in the verification:
- challenge
- public keys
- signature
-
The verify auth challenge trigger updates the used public key in the credential store if necessary.
-
The verify auth challenge trigger accepts the public key credential.
-
AWS Cognito returns tokens to the authentication finish endpoint.
The tokens consist of:
- access token
- ID token
- refresh token
-
The authentication finish endpoint returns the tokens to your app.
Sequence diagram:
sequenceDiagram
actor User
participant Authenticator
participant YourApp
participant DiscoverableEndpoint
participant AuthenticationStartEndpoint
participant AuthenticationFinishEndpoint
participant Cognito
participant DefineAuthChallenge
participant CreateAuthChallenge
participant VerifyAuthChallenge
participant SessionStore
participant CredentialStore
YourApp-)User: Show the sign-in page
activate YourApp
YourApp->>DiscoverableEndpoint: POST /authentication/discover
activate DiscoverableEndpoint
DiscoverableEndpoint->>DiscoverableEndpoint: Create a public key credential request options
DiscoverableEndpoint->>SessionStore: Store a discoverable authentication session
activate SessionStore
SessionStore-->>DiscoverableEndpoint: Discoverable authentication session
deactivate SessionStore
DiscoverableEndpoint-->>YourApp: Public key credential request options
deactivate DiscoverableEndpoint
YourApp-)Authenticator: Initiate a discoverable credential request
deactivate YourApp
Authenticator-)User: Ask to select a passkey
User-)Authenticator: Select a passkey
Authenticator-)User: Ask to authorize the use of the passkey
User-)Authenticator: Authorize the use of the passkey
Authenticator->>Authenticator: Sign the challenge
Authenticator-)YourApp: Public key credential
activate YourApp
YourApp->>YourApp: Extract the user ID
YourApp->>AuthenticationStartEndpoint: POST /authentication/start
activate AuthenticationStartEndpoint
AuthenticationStartEndpoint->>Cognito: InitiateAuth
activate Cognito
Cognito->>DefineAuthChallenge: Invoke
activate DefineAuthChallenge
DefineAuthChallenge-->>Cognito: Initiate a custom authentication flow
deactivate DefineAuthChallenge
Cognito->>CreateAuthChallenge: Invoke
activate CreateAuthChallenge
CreateAuthChallenge-->>Cognito: Dummy challenge
deactivate CreateAuthChallenge
Cognito-->>AuthenticationStartEndpoint: Session ID, public key credential request options
deactivate Cognito
AuthenticationStartEndpoint-->>YourApp: Session ID, public key credential request options
deactivate AuthenticationStartEndpoint
YourApp->>AuthenticationFinishEndpoint: POST /authentication/finish
activate AuthenticationFinishEndpoint
AuthenticationFinishEndpoint->>Cognito: RespondToAuthChallenge
activate Cognito
Cognito->>VerifyAuthChallenge: Invoke
activate VerifyAuthChallenge
VerifyAuthChallenge->>SessionStore: Pop the discoverable authentication session
activate SessionStore
SessionStore-->>VerifyAuthChallenge: Discoverable authentication session
deactivate SessionStore
VerifyAuthChallenge->>CredentialStore: Query public keys
activate CredentialStore
CredentialStore-->>VerifyAuthChallenge: Public keys
deactivate CredentialStore
VerifyAuthChallenge->>VerifyAuthChallenge: Verify the public key credential
opt The public key has been updated
VerifyAuthChallenge->>CredentialStore: Update the public key
activate CredentialStore
CredentialStore-->>VerifyAuthChallenge: OK
deactivate CredentialStore
end
VerifyAuthChallenge-->>Cognito: Accept
deactivate VerifyAuthChallenge
Cognito-->>AuthenticationFinishEndpoint: Tokens
deactivate Cognito
AuthenticationFinishEndpoint-->>YourApp: Tokens
deactivate AuthenticationFinishEndpoint
YourApp-)User: OK
deactivate YourApp
A user may sign in to your app with a passkey stored in another device through cross-device authentication. In cross-device authentication, Step 8 through Step 12 will be replaced with the following steps:
-
The user selects authentication with another device.
-
User's authenticator provides a medium for the user to select a passkey from another device.
Usually, the medium is a combination of a QR code and Bluetooth.
-
The user connects another device to user's authenticator.
-
Another device obtains the public key credential request options from user's authenticator.
-
Another device asks the user to select a passkey from those associated with the relying party ID in another device.
A passkey includes a key pair of a private key and a public key.
-
The user selects a passkey.
-
Another device asks the user to authorize the use of the passkey.
-
The user authorizes the use of the passkey; e.g., using biometrics.
-
Another device signs the challenge with the private key → the signature.
-
Another device returns a public key credential which includes the public key and the signature to your app.
Sequence diagram:
sequenceDiagram
actor User
participant Authenticator
participant Another Device
participant YourApp
User-)Authenticator: Select authentication with another device
Authenticator-)User: Provide a medium to select a passkey in another device
User-)Another Device: Connect to the authenticator
Another Device-)Authenticator: Obtain the public key credential request options
Another Device-)User: Ask to select a passkey
User-)Another Device: Select a passkey
Another Device-)User: Ask to authorize the use of the passkey
User-)Another Device: Authorize the use of the passkey
Another Device->>Another Device: Sign the challenge
Another Device-)YourApp: Public key credential
-
A user has previously signed in to your app with a passkey.
-
Your app shows a web page for a user to sign in.
-
Your app restores the user ID of the user from the browser storage; e.g., the local storage.
-
Your app POSTs the user ID to the authentication start endpoint (
/authentication/start
). -
The authentication start endpoint calls the InitiateAuth AWS Cognito API with the following parameters:
AuthFlow
:"CUSTOM_AUTH"
AuthParameters
:USERNAME
: user ID
-
AWS Cognito invokes the define auth challenge trigger.
-
The define auth challenge trigger initiates a custom authentication flow.
-
AWS Cognito invokes the create auth challenge trigger.
-
The create auth challenge trigger lists the public keys associated with the user ID in the credential store → registered public keys.
-
The create auth challenge trigger creates a public key credential request options which include a challenge, a relying party ID, and the registered public keys.
-
The create auth challenge trigger returns the public key credential request options to AWS Cognito.
-
AWS Cognito returns a session ID and the public key credential request options to the authentication start endpoint.
-
The authentication start endpoint returns the session ID and the public key credential request options to your app.
-
Your app initiates a credential request with the public key credential request options.
-
User's authenticator asks the user to select a passkey from those associated with the relying party ID and the registered public keys in user's authenticator.
A passkey includes a key pair of a private key and a public key.
-
The user selects a passkey.
-
User's authenticator asks the user to authorize the user of the passkey.
-
The user authorizes the use of the passkey; e.g., using biometrics.
-
User's authenticator signs the challenge with the private key → the signature.
-
User's authenticator returns a public key credential which includes the public key and the signature to your app.
-
Your app POSTs the following parameters to the authentication finish endpoint (
/authentication/finish
):- session ID
- public key credential
- user ID
-
The authentication finish endpoint calls the RespondToAuthChallenge AWS Cognito API with the following parameters:
ChallengeName
:"CUSTOM_CHALLENGE"
Session
: session IDChallengeResponses
:USERNAME
: user IDANSWER
: public key credential
-
AWS Cognito invokes the verify auth challenge trigger.
-
The verify auth challenge trigger verifies the public key credential.
The following parameters are involved in the verification:
- challenge
- registered public keys
- signature
-
The verify auth challenge trigger updates the used public key in the credential store if necessary.
-
The verify auth challenge trigger accepts the public key credential.
-
AWS Cognito returns tokens to the authentication finish endpoint.
The tokens consist of:
- access token
- ID token
- refresh token
-
The authentication finish endpoint returns the tokens to your app.
Sequence diagram:
sequenceDiagram
actor User
participant Authenticator
participant YourApp
participant AuthenticationStartEndpoint
participant AuthenticationFinishEndpoint
participant Cognito
participant DefineAuthChallenge
participant CreateAuthChallenge
participant VerifyAuthChallenge
participant CredentialStore
User-)YourApp: Sign in
YourApp-)User: Show the sign-in page
activate YourApp
YourApp->>YourApp: Restore the user ID
YourApp->>AuthenticationStartEndpoint: POST /authentication/start
activate AuthenticationStartEndpoint
AuthenticationStartEndpoint->>Cognito: InitiateAuth
activate Cognito
Cognito->>DefineAuthChallenge: Invoke
activate DefineAuthChallenge
DefineAuthChallenge-->>Cognito: Initiate a custom authentication flow
deactivate DefineAuthChallenge
Cognito->>CreateAuthChallenge: Invoke
activate CreateAuthChallenge
CreateAuthChallenge->>CredentialStore: Query public keys
activate CredentialStore
CredentialStore-->>CreateAuthChallenge: Registered public keys
deactivate CredentialStore
CreateAuthChallenge->>CreateAuthChallenge: Create a public key credential request options
CreateAuthChallenge-->>Cognito: Public key credential request options
deactivate CreateAuthChallenge
Cognito-->>AuthenticationStartEndpoint: Session ID, public key credential request options
deactivate Cognito
AuthenticationStartEndpoint-->>YourApp: Session ID, public key credential request options
deactivate AuthenticationStartEndpoint
YourApp-)Authenticator: Initiate a credential request
deactivate YourApp
Authenticator-)User: Ask to select a passkey
User-)Authenticator: Select a passkey
Authenticator-)User: Ask to authorize the use of the passkey
User-)Authenticator: Authorize the use of the passkey
Authenticator->>Authenticator: Sign the challenge
Authenticator-)YourApp: Public key credential
activate YourApp
YourApp->>AuthenticationFinishEndpoint: POST /authentication/finish
activate AuthenticationFinishEndpoint
AuthenticationFinishEndpoint->>Cognito: RespondToAuthChallenge
activate Cognito
Cognito->>VerifyAuthChallenge: Invoke
activate VerifyAuthChallenge
VerifyAuthChallenge->>VerifyAuthChallenge: Verify the public key credential
opt The public key has been updated
VerifyAuthChallenge->>CredentialStore: Update the public key
activate CredentialStore
CredentialStore-->>VerifyAuthChallenge: OK
deactivate CredentialStore
end
VerifyAuthChallenge-->>Cognito: Accept
deactivate VerifyAuthChallenge
Cognito-->>AuthenticationFinishEndpoint: Tokens
deactivate Cognito
AuthenticationFinishEndpoint-->>YourApp: Tokens
deactivate AuthenticationFinishEndpoint
YourApp-)User: OK
deactivate YourApp
Except for the following materials licensed under CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/):