Skip to content

Commit c2f970e

Browse files
marius-se0xTim
andauthored
Docc improvements (#25)
* add index and example implementation articles * add more graphics * update README * remove documentation paragraph * Update Sources/WebAuthn/Docs.docc/index.md Co-authored-by: Tim Condon <[email protected]> * Update Sources/WebAuthn/Docs.docc/index.md Co-authored-by: Tim Condon <[email protected]> --------- Co-authored-by: Tim Condon <[email protected]>
1 parent 3f33612 commit c2f970e

File tree

12 files changed

+323
-146
lines changed

12 files changed

+323
-146
lines changed

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ let package = Package(
2727
.package(url: "https://github.com/unrelentingtech/SwiftCBOR.git", from: "0.4.5"),
2828
.package(url: "https://github.com/apple/swift-crypto.git", from: "2.0.0"),
2929
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
30-
.package(url: "https://github.com/apple/swift-certificates.git", branch: "main")
30+
.package(url: "https://github.com/apple/swift-certificates.git", from: "0.3.0"),
31+
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.1.0")
3132
],
3233
targets: [
3334
.target(

README.md

Lines changed: 8 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -21,51 +21,12 @@ and `WebAuthn` dependency to your target:
2121
.target(name: "MyApp", dependencies: [.product(name: "WebAuthn", package: "webauthn-swift")])
2222
```
2323

24-
## Usage
25-
26-
The library exposes just four core methods through the `WebAuthnManager` type:
27-
28-
- `WebAuthnManager.beginRegistration()`
29-
- `WebAuthnManager.finishRegistration()`
30-
- `WebAuthnManager.beginAuthentication()`
31-
- `WebAuthnManager.finishAuthentication()`
32-
33-
Generally, the library makes the following assumptions about how a Relying Party implementing this library will
34-
interface with a client that will handle calling the WebAuthn API:
35-
36-
1. JSON is the preferred data type for transmitting registration and authentication options from the server to
37-
the client to feed to `navigator.credentials.create()` and `navigator.credentials.get()` respectively.
38-
39-
2. JSON is the preferred data type for transmitting WebAuthn responses from the client to the Relying Party.
40-
41-
3. Bytes are not directly transmittable in either direction as JSON, and so should be encoded to and decoded
42-
using Base64 URL encoding. To make life a little bit easier there are two typealiases indicating whether
43-
something is expected, or returned, as base64/base64url:
44-
45-
- `public typealias URLEncodedBase64 = String`
46-
- `public typealias EncodedBase64 = String`
47-
48-
## Limitations
49-
50-
There are a few things this library currently does **not** support:
51-
52-
1. Currently RSA public keys are not support, we do however plan to add support for that. RSA keys are necessary for
53-
compatibility with Microsoft Windows platform authenticators.
54-
55-
2. Octet key pairs are not supported.
56-
57-
3. Attestation verification is currently not supported, we do however plan to add support for that. Some work has been
58-
done already, but there are more pieces missing. In most cases attestation verification is not recommended since it
59-
causes a lot of overhead. [From Yubico](https://developers.yubico.com/WebAuthn/WebAuthn_Developer_Guide/Attestation.html):
60-
> "If a service does not have a specific need for attestation information, namely a well defined policy for what to
61-
do with it and why, it is not recommended to verify authenticator attestations"
62-
6324
### Setup
6425

65-
Configure your backend with a `WebAuthnManager` instance:
26+
Configure your Relying Party with a `WebAuthnManager` instance:
6627

6728
```swift
68-
app.webAuthn = WebAuthnManager(
29+
let webAuthnManager = WebAuthnManager(
6930
config: WebAuthnConfig(
7031
relyingPartyDisplayName: "My Fancy Web App",
7132
relyingPartyID: "example.com",
@@ -77,114 +38,17 @@ app.webAuthn = WebAuthnManager(
7738

7839
### Registration
7940

80-
Scenario: A user wants to signup on a website using WebAuthn.
81-
82-
#### Explanation
41+
For a registration ceremony use the following two methods:
8342

84-
1. When tapping the "Register" button the client sends a request to
85-
the backend. The backend responds to this request with a call to `begingRegistration(user:)` which then returns a
86-
new `PublicKeyCredentialRequestOptions`. This must be send back to the client so it can pass it to
87-
`navigator.credentials.create()`.
88-
89-
2. Whatever `navigator.credentials.create()` returns will be send back to the backend, parsing it into
90-
`RegistrationCredential`.
91-
```swift
92-
let registrationCredential = try req.content.decode(RegistrationCredential.self)
93-
```
94-
95-
3. Next the backend calls `finishRegistration(challenge:credentialCreationData:)` with the previously
96-
generated challenge and the received `RegistrationCredential`. If `finishRegistration` succeeds a new `Credential`
97-
object will be returned. This object contains information about the new credential, including an id and the generated public-key. Persist this data in e.g. a database and link the entry to the user.
98-
99-
##### Example implementation (using Vapor)
100-
101-
```swift
102-
authSessionRoutes.get("makeCredential") { req -> PublicKeyCredentialCreationOptions in
103-
let user = try req.auth.require(User.self)
104-
let options = try req.webAuthn.beginRegistration(user: user)
105-
req.session.data["challenge"] = options.challenge
106-
return options
107-
}
108-
109-
authSessionRoutes.post("makeCredential") { req -> HTTPStatus in
110-
let user = try req.auth.require(User.self)
111-
guard let challenge = req.session.data["challenge"] else { throw Abort(.unauthorized) }
112-
let registrationCredential = try req.content.decode(RegistrationCredential.self)
113-
114-
let credential = try await req.webAuthn.finishRegistration(
115-
challenge: challenge,
116-
credentialCreationData: registrationCredential,
117-
// this is likely to be removed soon
118-
confirmCredentialIDNotRegisteredYet: { credentialID in
119-
try await queryCredentialWithUser(id: credentialID) == nil
120-
}
121-
)
122-
123-
try await WebAuthnCredential(from: credential, userID: user.requireID())
124-
.save(on: req.db)
125-
126-
return .ok
127-
}
128-
```
43+
- `WebAuthnManager.beginRegistration()`
44+
- `WebAuthnManager.finishRegistration()`
12945

13046
### Authentication
13147

132-
Scenario: A user wants to log in on a website using WebAuthn.
133-
134-
#### Explanation
135-
136-
1. When tapping the "Login" button the client sends a request to
137-
the backend. The backend responds to this request with a call to `beginAuthentication()` which then in turn
138-
returns a new `PublicKeyCredentialRequestOptions`. This must be sent back to the client so it can pass it to
139-
`navigator.credentials.get()`.
140-
2. Whatever `navigator.credentials.get()` returns will be sent back to the backend, parsing it into
141-
`AuthenticationCredential`.
142-
```swift
143-
let authenticationCredential = try req.content.decode(AuthenticationCredential.self)
144-
```
145-
3. Next the backend calls
146-
`finishAuthentication(credential:expectedChallenge:credentialPublicKey:credentialCurrentSignCount:)`.
147-
- The `credential` parameter expects the decoded `AuthenticationCredential`
148-
- The `expectedChallenge` parameter expects the challenge previously generated
149-
from `beginAuthentication()` (obtained e.g. through a session).
150-
- Query the persisted credential from [Registration](#registration) using the credential id from the decoded
151-
`AuthenticationCredential`. Pass this credential in the `credentialPublicKey` parameter and it's sign count to
152-
`credentialCurrentSignCount`.
153-
154-
4. If `finishAuthentication` succeeds you can safely login the user linked to the credential! `finishAuthentication`
155-
will return a `VerifiedAuthentication` with the updated sign count and a few other pieces of information to be
156-
persisted. Use this to update the credential in the database.
157-
158-
#### Example implementation
159-
160-
```swift
161-
// this endpoint will be called on clicking "Login"
162-
authSessionRoutes.get("authenticate") { req -> PublicKeyCredentialRequestOptions in
163-
let options = try req.webAuthn.beginAuthentication()
164-
req.session.data["challenge"] = String.base64URL(fromBase64: options.challenge)
165-
return options
166-
}
167-
168-
// this endpoint will be called after the user used e.g. TouchID.
169-
authSessionRoutes.post("authenticate") { req -> HTTPStatus in
170-
guard let challenge = req.session.data["challenge"] else { throw Abort(.unauthorized) }
171-
let data = try req.content.decode(AuthenticationCredential.self)
172-
guard let credential = try await queryCredentialWithUser(id: data.id) else {
173-
throw Abort(.unauthorized)
174-
}
175-
176-
let verifiedAuthentication = try req.webAuthn.finishAuthentication(
177-
credential: data,
178-
expectedChallenge: challenge,
179-
credentialPublicKey: [UInt8](credential.publicKey.base64URLDecodedData!),
180-
credentialCurrentSignCount: 0
181-
)
48+
For an authentication ceremony use the following two methods:
18249

183-
req.auth.login(credential.user)
184-
185-
return .ok
186-
}
187-
```
50+
- `WebAuthnManager.beginAuthentication()`
51+
- `WebAuthnManager.finishAuthentication()`
18852

18953
## Credits
19054

Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ import Foundation
1616
import Crypto
1717

1818
/// The unprocessed response received from `navigator.credentials.create()`.
19-
/// Internally this will be parsed into a more readable `ParsedCredentialCreationResponse`.
2019
public struct RegistrationCredential: Codable {
20+
/// The credential ID of the newly created credential.
2121
public let id: String
22+
/// Value will always be "public-key" (for now)
2223
public let type: String
24+
/// The raw credential ID of the newly created credential.
2325
public let rawID: URLEncodedBase64
26+
/// The attestation response from the authenticator.
2427
public let attestationResponse: AuthenticatorAttestationResponse
2528

2629
enum CodingKeys: String, CodingKey {
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Example Implementation
2+
3+
Explains how to use this library in a server <-> website scenario.
4+
5+
The library exposes four methods through ``WebAuthnManager``:
6+
7+
- ``WebAuthnManager/beginRegistration(user:timeout:attestation:publicKeyCredentialParameters:)``
8+
- ``WebAuthnManager/finishRegistration(challenge:credentialCreationData:requireUserVerification:supportedPublicKeyAlgorithms:pemRootCertificatesByFormat:confirmCredentialIDNotRegisteredYet:)``
9+
- ``WebAuthnManager/beginAuthentication(challenge:timeout:allowCredentials:userVerification:)``
10+
- ``WebAuthnManager/finishAuthentication(credential:expectedChallenge:credentialPublicKey:credentialCurrentSignCount:requireUserVerification:)``
11+
12+
Generally, the library makes the following assumptions about how a Relying Party implementing this library will
13+
interface with a client that will handle calling the WebAuthn API:
14+
15+
1. JSON is the preferred data type for transmitting registration and authentication options from the server to
16+
the client to feed to `navigator.credentials.create()` and `navigator.credentials.get()` respectively.
17+
18+
2. JSON is the preferred data type for transmitting WebAuthn responses from the client to the Relying Party.
19+
20+
3. Bytes are not directly transmittable in either direction as JSON, and so should be encoded to and decoded
21+
using Base64 URL encoding. To make life a little bit easier ``URLEncodedBase64`` and ``EncodedBase64`` indicate whether a `String` is currently encoded or not.
22+
23+
## Limitations
24+
25+
There are a few things this library currently does **not** support:
26+
27+
1. Currently RSA public keys are not support, we do however plan to add support for that. RSA keys are necessary for
28+
compatibility with Microsoft Windows platform authenticators.
29+
30+
2. Octet key pairs are not supported.
31+
32+
3. Attestation verification is currently not supported, we do however plan to add support for that. Some work has been
33+
done already, but there are more pieces missing. In most cases attestation verification is not recommended since it
34+
causes a lot of overhead. [From Yubico](https://developers.yubico.com/WebAuthn/WebAuthn_Developer_Guide/Attestation.html):
35+
> "If a service does not have a specific need for attestation information, namely a well defined policy for what to
36+
do with it and why, it is not recommended to verify authenticator attestations"
37+
38+
### Setup
39+
40+
Configure your backend with a ``WebAuthnManager`` instance:
41+
42+
```swift
43+
app.webAuthn = WebAuthnManager(
44+
config: WebAuthnConfig(
45+
relyingPartyDisplayName: "My Fancy Web App",
46+
relyingPartyID: "example.com",
47+
relyingPartyOrigin: "https://example.com",
48+
timeout: 600
49+
)
50+
)
51+
```
52+
53+
### Registration
54+
55+
Scenario: A user wants to signup on a website using WebAuthn.
56+
57+
![Registration flow overview](registration.svg)
58+
59+
#### Explanation
60+
61+
1. When tapping the "Register" button the client sends a request to
62+
the backend. The backend responds to this request with a call to `begingRegistration(user:)` which then returns a
63+
new ``PublicKeyCredentialRequestOptions``. This must be send back to the client so it can pass it to
64+
`navigator.credentials.create()`.
65+
66+
2. Whatever `navigator.credentials.create()` returns will be send back to the backend, parsing it into
67+
``RegistrationCredential``.
68+
```swift
69+
let registrationCredential = try req.content.decode(RegistrationCredential.self)
70+
```
71+
72+
3. Next the backend calls `finishRegistration(challenge:credentialCreationData:)` with the previously
73+
generated challenge and the received ``RegistrationCredential``. If `finishRegistration` succeeds a new ``Credential``
74+
object will be returned. This object contains information about the new credential, including an id and the generated public-key. Persist this data in e.g. a database and link the entry to the user.
75+
76+
##### Example implementation (using Vapor)
77+
78+
```swift
79+
authSessionRoutes.get("makeCredential") { req -> PublicKeyCredentialCreationOptions in
80+
let user = try req.auth.require(User.self)
81+
let options = try req.webAuthn.beginRegistration(user: user)
82+
req.session.data["challenge"] = options.challenge
83+
return options
84+
}
85+
86+
authSessionRoutes.post("makeCredential") { req -> HTTPStatus in
87+
let user = try req.auth.require(User.self)
88+
guard let challenge = req.session.data["challenge"] else { throw Abort(.unauthorized) }
89+
let registrationCredential = try req.content.decode(RegistrationCredential.self)
90+
91+
let credential = try await req.webAuthn.finishRegistration(
92+
challenge: challenge,
93+
credentialCreationData: registrationCredential,
94+
// this is likely to be removed soon
95+
confirmCredentialIDNotRegisteredYet: { credentialID in
96+
try await queryCredentialWithUser(id: credentialID) == nil
97+
}
98+
)
99+
100+
try await WebAuthnCredential(from: credential, userID: user.requireID())
101+
.save(on: req.db)
102+
103+
return .ok
104+
}
105+
```
106+
107+
### Authentication
108+
109+
Scenario: A user wants to log in on a website using WebAuthn.
110+
111+
![Authentication flow overview](authentication.svg)
112+
113+
#### Explanation
114+
115+
1. When tapping the "Login" button the client sends a request to
116+
the backend. The backend responds to this request with a call to `beginAuthentication()` which then in turn
117+
returns a new ``PublicKeyCredentialRequestOptions``. This must be sent back to the client so it can pass it to
118+
`navigator.credentials.get()`.
119+
2. Whatever `navigator.credentials.get()` returns will be sent back to the backend, parsing it into
120+
``AuthenticationCredential``.
121+
```swift
122+
let authenticationCredential = try req.content.decode(AuthenticationCredential.self)
123+
```
124+
3. Next the backend calls
125+
``WebAuthnManager/finishAuthentication(credential:expectedChallenge:credentialPublicKey:credentialCurrentSignCount:requireUserVerification:)``.
126+
- The `credential` parameter expects the decoded ``AuthenticationCredential``
127+
- The `expectedChallenge` parameter expects the challenge previously generated
128+
from `beginAuthentication()` (obtained e.g. through a session).
129+
- Query the persisted credential from [Registration](#registration) using the credential id from the decoded
130+
`AuthenticationCredential`. Pass this credential in the `credentialPublicKey` parameter and it's sign count to
131+
`credentialCurrentSignCount`.
132+
133+
4. If `finishAuthentication` succeeds you can safely login the user linked to the credential! `finishAuthentication`
134+
will return a `VerifiedAuthentication` with the updated sign count and a few other pieces of information to be
135+
persisted. Use this to update the credential in the database.
136+
137+
#### Example implementation
138+
139+
```swift
140+
// this endpoint will be called on clicking "Login"
141+
authSessionRoutes.get("authenticate") { req -> PublicKeyCredentialRequestOptions in
142+
let options = try req.webAuthn.beginAuthentication()
143+
req.session.data["challenge"] = String.base64URL(fromBase64: options.challenge)
144+
return options
145+
}
146+
147+
// this endpoint will be called after the user used e.g. TouchID.
148+
authSessionRoutes.post("authenticate") { req -> HTTPStatus in
149+
guard let challenge = req.session.data["challenge"] else { throw Abort(.unauthorized) }
150+
let data = try req.content.decode(AuthenticationCredential.self)
151+
guard let credential = try await queryCredentialWithUser(id: data.id) else {
152+
throw Abort(.unauthorized)
153+
}
154+
155+
let verifiedAuthentication = try req.webAuthn.finishAuthentication(
156+
credential: data,
157+
expectedChallenge: challenge,
158+
credentialPublicKey: [UInt8](credential.publicKey.base64URLDecodedData!),
159+
credentialCurrentSignCount: 0
160+
)
161+
162+
req.auth.login(credential.user)
163+
164+
return .ok
165+
}
166+
```

0 commit comments

Comments
 (0)