Server Configuration¶
Configuring a server to use this library requires doing a few things:
Implementing the CredentialsRegistrar interface from webauthn-rp.
Setting up a database to store the registered credential public keys and metadata.
Configuring and handling routes for the user client to:
Begin registration for an authenticator.
Send back the attested registration response.
Begin authentication with an authenticator.
Send back the asserted authentication response.
You can use the same route to handle all the client requests and responses, though in this example we’ll split them up for clarity.
Also, we’ll use the Flask lightweight Python web framework along with an extension for the SQLAlchemy object relational mapping framework. You can install them both using pip:
pip install Flask Flask-SQLAlchemy
Registrar Overview¶
Before creating the database models it is useful to take a look at the CredentialsRegistrar interface to understand what kinds of data it needs to store and retrieve.
class CredentialsRegistrar:
"""A registrar for public key credentials.
This class specifies the interface between the `CredentialsBackend` and the
Relying Party's credentials storage and processing layer.
The provided methods will be invoked by the `CredentialsBackend` at
specific points during the user registration and user authentication
phases.
"""
def register_credential_attestation(
self,
credential: PublicKeyCredential,
att: AttestationObject,
att_type: AttestationType,
user: PublicKeyCredentialUserEntity,
rp: PublicKeyCredentialRpEntity,
trusted_path: Optional[TrustedPath] = None) -> Any:
"""Registers the attempted attestation of a credential by a user.
This is the last step in the user registration ceremony which was
initiated by the user agent. Successful completion indicates that the
user's credential has been stored and is ready for authentication.
Args:
credential (PublicKeyCredential): The public key credential to
associate with a user and Relying Party.
att (AttestationObject): The attestation object associated with the
given public key credential.
att_type (AttestationType): The type of attestation that was
confirmed by the `CredentialsBackend`.
user (PublicKeyCredentialUserEntity): The user to associate with
the public key credential.
rp (PublicKeyCredentialRpEntity): The Relying Party to associate with
the public key credential.
trusted_path (Optional[TrustedPath]): The optional trusted path
for the credential and attestation object provided by the
`CredentialsBackend`.
Returns:
None for success and anything else to indicate an error.
"""
raise UnimplementedError('Must implement register_credential_creation')
def register_credential_assertion(self, credential: PublicKeyCredential,
authenticator_data: AuthenticatorData,
user: PublicKeyCredentialUserEntity,
rp: PublicKeyCredentialRpEntity) -> Any:
"""Registers the attempted assertion of a credential by a user.
This is the last step in the user authentication ceremony which was
initiated by the user agent. Successful completion indicates that the
any necessary state related to the user's credential was updated and
the authentication process can finish.
Args:
credential (PublicKeyCredential): The public key credential
associated with the given user and Relying Party.
authenticator_data (AuthenticatorData): The parsed authenticator
data.
user (PublicKeyCredentialUserEntity): The user associated with
the public key credential.
rp (PublicKeyCredentialRpEntity): The Relying Party associated with
the public key credential.
Returns:
None for success and anything else to indicate an error.
"""
raise UnimplementedError('Must implement register_credential_request')
def get_credential_data(self,
credential_id: bytes) -> Optional[CredentialData]:
"""Gets the `CredentialData` associated with a specific credential.
Args:
credential_id (bytes): The probabilistically-unique credential ID.
Returns:
The `CredentialData` associated with the given ID or None if it
does not exist.
References:
* https://w3.org/TR/webauthn/#credential-id
"""
raise UnimplementedError('Must implement get_credential_data')
Focusing on the get_credential_data function, notice that you’ll need to be able to retrieve a number of fields related to a particular credential.
Note
Each credential can be identified using a byte string that is at least 16 bytes long and is probabilistically unique. The specific data you’ll want to retrieve is enumerated in the CredentialData NamedTuple shown below. The first three fields, credential_public_key, signature_count, and user_entity are required.
class CredentialData(NamedTuple):
"""Information stored about a specific user credential.
Attributes:
credential_public_key (CredentialPublicKey): The public key associated
with a particular credential.
signature_count (Optional[int]): The current signature count of a
credential if one has been registered. It should be None if it has not
been initialized yet (right after the creation of a credential).
user_entity (PublicKeyCredentialUserEntity): The user that owns the
credential.
rp_entity (Optional[PublicKeyCredentialRpEntity]): The optional Relying
Party that is associated with this credential.
"""
credential_public_key: CredentialPublicKey
signature_count: Optional[int]
user_entity: PublicKeyCredentialUserEntity
rp_entity: Optional[PublicKeyCredentialRpEntity] = None
How you store the credential_public_key in the database is your choice, however, considering that it is represented in the specification using the COSE_Key CBOR (Concise Binary Object Representation) format, that is the compact format that is recommended, especially if you just want to store a binary blob. This library also contains some utility functions to convert to and from this particular encoding (used below).
Note
A user handle is a byte string that the Relying Party uses to identify the user but should contain no personally identifiable information, i.e. not a username or email address.
The data to be stored is provided in the two register functions. In particular you can find the credential_public_key using the att parameter under att.auth_data.attested_credential_data.credential_public_key and the signature_count under att.auth_data.sign_count. Additionally, the credential_id is under att.auth_data.attested_credential_data.credential_id.
Lastly, although not explicitly retrieved using a get function, you’ll need to store the challenge that is used for each registration and authentication ceremony in order to have the CredentialsBackend check it for verification. The challenge is provided in the options object under options.public_key.challenge.
Flask Setup¶
To configure Flask create a file app.py in a work directory with the following:
from flask import Flask
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/example.db'
db = SQLAlchemy(app)
The setups up the app and database engine.
Database Models¶
From the previous sections, the data required for retrieval has been established and so now we can move on to building the database models.
The user model is quite simple and just contains:
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(32), unique=True)
user_handle = db.Column(db.String(64), unique=True)
credentials = db.relationship('Credential',
backref=db.backref('user', lazy=True))
challenges = db.relationship('Challenge',
backref=db.backref('user', lazy=True))
@staticmethod
def by_user_handle(user_handle: bytes) -> Optional['User']:
return User.query.filter_by(user_handle=user_handle).first()
Similarly the credential model is:
class Credential(db.Model):
id = db.Column(db.String(), primary_key=True)
signature_count = db.Column(db.Integer, nullable=True)
credential_public_key = db.Column(db.String)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
You’ll also need to store the challenge information that was used during registration and authentication so that you are able to verify it.
class Challenge(db.Model):
id = db.Column(db.Integer, primary_key=True)
request = db.Column(db.String, unique=True)
timestamp_ms = db.Column(db.Integer)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
That’s the bare minimum you’ll need in order to get started. Next, we’ll revisit the registrar in order to implement the required functions.
Implementing the Registrar¶
Although the register functions are passed a lot of data, we’ll only focus on the key pieces of information that need to be stored for later retrieval as previously mentioned.
Putting it all together yields:
class RegistrarImpl(CredentialsRegistrar):
def register_credential_attestation(
self,
credential: PublicKeyCredential,
att: AttestationObject,
att_type: AttestationType,
user: PublicKeyCredentialUserEntity,
rp: PublicKeyCredentialRpEntity,
trusted_path: Optional[TrustedPath] = None) -> Any:
assert att.auth_data is not None
assert att.auth_data.attested_credential_data is not None
cpk = att.auth_data.attested_credential_data.credential_public_key
user_model = User.by_user_handle(user.id)
if user_model is None: return 'No user found'
credential_model = Credential()
credential_model.id = credential.raw_id
credential_model.signature_count = None
credential_model.credential_public_key = cose_key(cpk)
credential_model.user = user_model
db.session.add(credential_model)
db.session.commit()
def register_credential_assertion(self, credential: PublicKeyCredential,
authenticator_data: AuthenticatorData,
user: PublicKeyCredentialUserEntity,
rp: PublicKeyCredentialRpEntity) -> Any:
credential_model = Credential.query.filter_by(
id=credential.raw_id).first()
credential_model.signature_count = authenticator_data.sign_count
db.session.commit()
def get_credential_data(self,
credential_id: bytes) -> Optional[CredentialData]:
credential_model = Credential.query.filter_by(id=credential_id).first()
if credential_model is None:
return None
return CredentialData(
parse_cose_key(credential_model.credential_public_key),
credential_model.signature_count,
PublicKeyCredentialUserEntity(
name=credential_model.user.username,
id=credential_model.user.user_handle,
display_name=credential_model.user.username))
Next, we’ll go about creating and handling the registration and authentication routes.
Registration Request¶
When a user client wants to register an authenticator with a Relying Party, it’ll first need to request some options from the Relying Party that specify a number of things, namely what kinds of authenticators are acceptable and which challenge should be used. In this particular example, the user will be registering for the first time and so also provides a desired username.
@app.route('/registration/request/', methods=['POST'])
def registration_request():
username = request.form['username']
user_model = User.query.filter_by(username=username).first()
if user_model is not None:
user_handle = user_model.user_handle
else:
user_handle = secrets.token_bytes(64)
user_model = User()
user_model.username = username
user_model.user_handle = user_handle
db.session.add(user_model)
db.session.commit()
challenge_bytes = secrets.token_bytes(64)
challenge = Challenge()
challenge.request = challenge_bytes
challenge.timestamp_ms = timestamp_ms()
challenge.user_id = user_model.id
db.session.add(challenge)
db.session.commit()
options = APP_CCO_BUILDER.build(
user=PublicKeyCredentialUserEntity(name=username,
id=user_handle,
display_name=username),
challenge=challenge_bytes,
)
options_json = jsonify(options)
response_json = {
'challengeID': challenge.id,
'creationOptions': options_json,
}
response_json_string = json.dumps(response_json)
return (response_json_string, 200, {'Content-Type': 'application/json'})
To reidentify the challenge, note that we’re also sending a unique ID with the JSON response.
Registration Response¶
If the client successfully is able to use the creation options from the registration request to generate an attestation on the user’s behalf, then this endpoint will verify the attestation and finish the user’s registration if valid.
@app.route('/registration/response/', methods=['POST'])
def registration_response():
try:
challengeID = request.form['challengeID']
credential = parse_public_key_credential(
json.loads(request.form['credential']))
username = request.form['username']
except Exception:
return ('Could not parse input data', 400)
if type(credential.response) is not AuthenticatorAttestationResponse:
return ('Invalid response type', 400)
challenge_model = Challenge.query.filter_by(id=challengeID).first()
if not challenge_model:
return ('Could not find challenge matching given id', 400)
user_model = User.query.filter_by(username=username).first()
if not user_model:
return ('Invalid username', 400)
current_timestamp = timestamp_ms()
if current_timestamp - challenge_model.timestamp_ms > APP_TIMEOUT:
return ('Timeout', 408)
user_entity = PublicKeyCredentialUserEntity(name=username,
id=user_model.user_handle,
display_name=username)
try:
APP_CREDENTIALS_BACKEND.handle_credential_attestation(
credential=credential,
user=user_entity,
rp=APP_RELYING_PARTY,
expected_challenge=challenge_model.request,
expected_origin=APP_ORIGIN)
except WebAuthnRPError:
return ('Could not handle credential attestation', 400)
return ('Success', 200)
Along with using the credentials backend to verify the challenge and the rest of the attestation, object we’re also checking to make sure that the response was sent before the specified timeout.
Authentication Request¶
The authentication flow is very much like the registration flow, just that credential request options are returned instead of credential creation options.
@app.route('/authentication/request/', methods=['POST'])
def authentication_request():
username = request.form['username']
user_model = User.query.filter_by(username=username).first()
if user_model is None:
return ('User not registered', 400)
credential_models = Credential.query.filter_by(user_id=user_model.id).all()
print('found models', len(credential_models))
if credential_models is None:
return ('User without credential', 400)
challenge_bytes = secrets.token_bytes(64)
challenge = Challenge()
challenge.request = challenge_bytes
challenge.timestamp_ms = timestamp_ms()
challenge.user_id = user_model.id
db.session.add(challenge)
db.session.commit()
options = APP_CRO_BUILDER.build(
challenge=challenge_bytes,
allow_credentials=[
PublicKeyCredentialDescriptor(
id=credential_model.id,
type=PublicKeyCredentialType.PUBLIC_KEY,
) for credential_model in credential_models
])
options_json = jsonify(options)
response_json = {
'challengeID': challenge.id,
'requestOptions': options_json,
}
response_json_string = json.dumps(response_json)
return (response_json_string, 200, {'Content-Type': 'application/json'})
Authentication Response¶
Finally, the authentication flow also mirrors the registration flow, just that an assertion is expected rather than an attestation object.
@app.route('/authentication/response/', methods=['POST'])
def authentication_response():
try:
challengeID = request.form['challengeID']
credential = parse_public_key_credential(
json.loads(request.form['credential']))
username = request.form['username']
except Exception:
return ('Could not parse input data', 400)
if type(credential.response) is not AuthenticatorAssertionResponse:
return ('Invalid response type', 400)
challenge_model = Challenge.query.filter_by(id=challengeID).first()
if not challenge_model:
return ('Could not find challenge matching given id', 400)
user_model = User.query.filter_by(username=username).first()
if not user_model:
return ('Invalid username', 400)
current_timestamp = timestamp_ms()
if current_timestamp - challenge_model.timestamp_ms > APP_TIMEOUT:
return ('Timeout', 408)
user_entity = PublicKeyCredentialUserEntity(name=username,
id=user_model.user_handle,
display_name=username)
try:
APP_CREDENTIALS_BACKEND.handle_credential_assertion(
credential=credential,
user=user_entity,
rp=APP_RELYING_PARTY,
expected_challenge=challenge_model.request,
expected_origin=APP_ORIGIN)
except WebAuthnRPError:
return ('Could not handle credential assertion', 400)
return ('Success', 200)