A challenge–response authentication is a family of protocols in which one party presents a question (“challenge”) and another party must provide a valid answer (“response”) to be authenticated. 10
DID Auth is a protocol that allows asking the controller of an account to sign a random message, thus demonstrating control of the account at the time of the question. This protocol used as a login method checks that the user controls the account at the time of access to the application.
Additionally, it allows the application to request specific data at the time of registration, for example the user’s email or phone number. These requested data follow a specific standard, which allows the client to provide it in a unified way and even cryptographically signed by a third party. This means that the application can ask the user for verifiable credentials, identifying them by their type in a unique way.
Nowadays, authentication is handled in a centralized way. Most applications delegate the process of authenticating users to third-party services, like OIDC, relaying the authenticity of that information on those services providers. The users’ information is not controlled by the users theirselves, the third-party services control it.
We believe users are not going to trust their private information to third-parties services anymore, nor applications will trust in data provided by centralized services. In addition, users will ask to disclose the minimum amount of information they can. This is the Web 3.0.
The motivation of this protocol is to provide user centric registration and authentication mechanisms to be used by services of the web 3.0 considering:
We identify two main type of services: permissioned services, those which require user to provide certain information to access it, and open services, which do not require extra info apart from the user’s blockchain address.
This protocol allows services to prove that the user is in control of their Decentralized Identifier -DID- 2 (thus, their blockchain account) and, optionally, enables the application require the user to share specific private information associated to their account (their Verifiable Credentials 7 ), in order to register them performing a custom business logic, enabling the service to verify that the shared information has been issued by reliable entities.
It is designed to:
This protocol was inspired by:
Initially, it is designed to use HTTPS as the message transport layer, but it can be ported to a different one.
Services usually register users before letting them in. This means the service requires some specific information to be shared by the user (ie: user’s email).
Requiring information to the user is OPTIONAL, it depends on the service needs. Some services may not need to register users before letting them enter in. This lets identify the service in one of the two groups: permissioned or open.
POST /request-signup { did }
to Service, where did
is User’s DID{ challenge, sdr? }
were sdr?
is the OPTIONAL selective disclosure request defined by the Service. The sdr
MUST be sent in a signed JWT format.sdr
, Client obtains the information required from the user’s desired service or platform (for example, from the RIF Data Vault), and builds a selective disclosure (response)personal_sign
as per EIP-19116
and EIP-15515
:<custom header set by the developer>
URL: <web domain>
Verification code: <challenge>
My credentials are: <array of JWT credentials separated by commas>
where <web domain>
is the site DNS domain and <array of JWT credentials>
is the selective disclosure (which is set if sdr
was asked*). For example
You are going to log in to Taringa.
URL: taringa.net
Verification code: 4531
My credentials are: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjpyc2s6MHhjMmE0MWY3NmNhY2ZhOTMzYzM0OTY5NzdmMjE2MDk0NGVmOGMyZGUzIiwicm9sZSI6IlJJRiBEZXZlbG9wZXIiLCJpc3MiOiJkaWQ6ZXRocjpyc2s6MHg0Y2MxNzc0MjI2NDNjMzgxNGE5ZThhNzY1NDk4NTIxYzUyMDRmMTExIiwiaWF0IjoxNTE2MjM5MDIyfQ.3sauMI60RVqc1QrvooZnNnmjAMiHj4qt5ZSEYhOULvA,eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjpyc2s6MHhjMmE0MWY3NmNhY2ZhOTMzYzM0OTY5NzdmMjE2MDk0NGVmOGMyZGUzIiwic2tpbGxzIjoiQmxvY2tjaGFpbiIsImlzcyI6ImRpZDpldGhyOnJzazoweDRjYzE3NzQyMjY0M2MzODE0YTllOGE3NjU0OTg1MjFjNTIwNGYxMTEiLCJpYXQiOjE1MTYyMzkwMjJ9.SgPPVFj0lU9E_dq_aPOmrf_CZljNh1ZaEhAufAbIgFY
POST /signup {
did: <signer's DID>,
sig: <message signature>,
credentials: <array of JWT credentials>
}
For the given example it is
{
"did": "did:ethr:rsk:0xa53...dec",
"sig": "...",
"credentials": [
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjpyc2s6MHhjMmE0MWY3NmNhY2ZhOTMzYzM0OTY5NzdmMjE2MDk0NGVmOGMyZGUzIiwicm9sZSI6IlJJRiBEZXZlbG9wZXIiLCJpc3MiOiJkaWQ6ZXRocjpyc2s6MHg0Y2MxNzc0MjI2NDNjMzgxNGE5ZThhNzY1NDk4NTIxYzUyMDRmMTExIiwiaWF0IjoxNTE2MjM5MDIyfQ.3sauMI60RVqc1QrvooZnNnmjAMiHj4qt5ZSEYhOULvA,eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjpyc2s6MHhjMmE0MWY3NmNhY2ZhOTMzYzM0OTY5NzdmMjE2MDk0NGVmOGMyZGUzIiwic2tpbGxzIjoiQmxvY2tjaGFpbiIsImlzcyI6ImRpZDpldGhyOnJzazoweDRjYzE3NzQyMjY0M2MzODE0YTllOGE3NjU0OTg1MjFjNTIwNGYxMTEiLCJpYXQiOjE1MTYyMzkwMjJ9.SgPPVFj0lU9E_dq_aPOmrf_CZljNh1ZaEhAufAbIgFY"
]
}
response
and recovers the signer against this message (Service needs the User’s DID to fetch the expected challenge):<custom header set by the developer>
URL: <service expected domain>
Verification code: <expected challenge>
My credentials are: {response.credentials.join(',')}
Then checks message signer matches response.did
’s address, and performs business logic over the selective disclosure. If business logic is successful, it logs the user in by creating an access token and a refresh token:
{
iss: `${serviceDid}`,
aud: `${serviceUrl}`,
sub: `${userDid}`,
exp: `${now + 10 min}`, // should be shorter than 15 minutes
nbf: `${now}`,
iat: `${now}`
}
Other claims could be useful for storing user metadata and other use case related information.
{ accessToken, refreshToken }
*The credential format is not friendly for the user: they cannot understand what they are signing. Future work will research on finding a more user-oriented form to display the JWTs but with current technology this is what we can do. Ideally EIP-712 could be used to show display JWTs in user-oriented form.
See authenticating requests to understand how to user access and refresh tokens.
The selective disclosure request is optional and it depends on the service needs.
challenge
is enough*The challenge may be non-deterministic, in that case, the service will have to store the challenges state. See How to calculate a deterministic challenge
It is strongly based on uPort implementation, where the service requires certain information and the user responds with it.
The selective disclosure request must be compatible with uPort DAF implementation, so it must implement the following interfaces.
interface Claim {
claimType: string
claimValue: string
reason?: string
essential?: boolean
}
interface SelectiveDisclosureRequest {
issuer: string
subject: string
replyUrl?: string
claims?: Claim[]
credentials?: string[]
}
issuer
: the service did. REQUIREDsubject
: the user did (the one received when requesting the challenge). REQUIREDreplyUrl
: the sign up endpoint.claims
: needed claims that may or not be part of a credential. IE: preferredLanguage
.credentials
: array of W3C Verifiable Credentials JSON Schema names. Those schemas definitions will be published in an open repository in Github. IE: EmailCredential
, BirthdateCredential
.interface SelectiveDisclosureResponse {
issuer: string
subject: string
claims?: Claim[]
credentials: VerifiableCredential[]
}
issuer
: the user did. REQUIREDsubject
: the service did. REQUIREDclaims
: requested claims. IE: { claimType: 'preferredLanguage', claimValue: 'english' }
credentials
: array of W3C Verifiable Credentials that implements the requested JSON schemasServices should use login after signin up users. This means the service already obtained the information required to let the user access the service.
POST /request-auth { did }
to Service, where did
is User’s DID{ challenge }
.personal_sign
as per EIP-19116
and EIP-15515
:<custom header set by the developer>
URL: <web domain>
Verification code: <challenge>
Client prompts the User to sign it with DID controller’s private key.
POST /auth {
did: <signer's DID>,
sig: <message signature>
}
response
and recovers the signer against this message:<custom header set by the developer>
URL: <service expected domain>
Verification code: <expected challenge>
My credentials are: {response.credentials.join(',')}
Then checks message signer matches response.did
’s address. If necessary, performs business logic over the did
and the information related to it saved by the Service. If it is a valid user, it creates an access token and a refresh token - see (sign up to understand required token JWT payload format)
See authenticating requests to understand how to user access and refresh tokens.
*The challenge may be non-deterministic, in that case, the service will have to store the challenges state. See How to calculate a deterministic challenge
After the user is registered and has logged in (meaning the user is holding an access token and a refresh token) the flow for authenticating following requests is:
"Expired access token"
string as HTTP body.POST /refresh-token
to Service including the refresh token. See how to send refresh tokens.{ accessToken, refreshToken }
This operation will invalidate the current user’s session. The next time /refresh-token
is invoked, it will not generate a new access token.
POST /logout
with the current _access tokenPOST /refresh-token
to Service with the refresh token
NOTE: The logout process does not invalidate the current access token, it will still be valid until it expires, that’s why it matters to implement short validity periods for access token. The logout just prevents the access token to be renewed.
@rsksmart/express-did-auth
- in progress/products
)sdr
is requestedCalculate a deterministic challenge prevents the server to maintain a state of emitted challenges while those challenges are valid for a certain amount of time.
Example:
const challengeExpirationTime = 5min
const serverSecret = 'this is the server super secret'
const userDid = `did:ethr:rsk:0x0123456789abcdef`
const timestamp = int(now / challengeExpirationTime)
const challenge = keccak256(userDid-serverSecret-timestamp`)
By doing this, the challenge
will be valid for the next challengeExpirationTime - now % challengeExpirationTime
. Once the user sends the signed challenge back to the service, the server MUST perform the same calculation and the received challenge must be coincident with the result of that new calculation, if not, an invalid challenge response will be sent.
There are different options to send the access token and the refresh token. Request headers, request body or cookies can be used depending on the case and the developer election, please find below the different descriptions.
NOTE: If you decide to use cookies, please make sure that your service is secure enough to prevent Cross Site Request Forgery (CSRF) atttacks.
It must be placed in the Authorization
header following the DIDAuth
scheme. This scheme will be present in HTTP Authentication Scheme Registry14
.
For example:
GET /resource HTTP/1.1
Host: server.example.com
Authorization: DIDAuth my.access.token
When the client performs a POST /auth
, the server must set the authorization
cookie with the following attributes: HttpOnly
, Secure
and SameSite=Strict
(this new attribute prevents CSRF, but it is not supported by all the browsers yet). See more information about cookies here
For example:
Set-Cookie: authorization=my.access.token; Secure; HttpOnly; SameSite=Strict
Then, the client browser will send the cookie on every request.
It must be placed in the body of the request as a refreshToken
field.
For example:
POST /refresh-token HTTP/1.1
Host: server.example.com
{
refreshToken: 'theRefreshToken'
}
When the client performs a POST /auth
, the server must set the refresh-token
cookie with the following attributes: HttpOnly
, Secure
and SameSite=Strict
.
For example:
Set-Cookie: refresh-token=theRefreshToken; Secure; HttpOnly; SameSite=Strict
Then, the client browser will send the cookie on every request, so when the client makes a POST /refresh-token
, the server will replace the existing cookies (authorization
and refresh-token
) with new values.
Go to top