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:

    1. Begin registration for an authenticator.

    2. Send back the attested registration response.

    3. Begin authentication with an authenticator.

    4. 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)