Skip to content

Commit 07359c8

Browse files
authored
Merge pull request #17 from swift-server/readme
First Readme Draft
2 parents 3cec1dc + f0f82bb commit 07359c8

File tree

1 file changed

+180
-1
lines changed

1 file changed

+180
-1
lines changed

README.md

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,184 @@
11
# webauthn-swift
22

3+
This package provides a Swift implementation of the [WebAuthn API](https://w3c.github.io/webauthn) focused on making it
4+
easy to leverage the power of WebAuthn.
5+
36
🚨 This library is a proof of concept - do not use it in production yet!
47

5-
A Swift library for implementing the WebAuthn spec
8+
## Getting Started
9+
10+
**Adding the dependency**
11+
12+
Add the following entry in your `Package.swift` to start using `WebAuthn`:
13+
14+
```swift
15+
.package(url: "https://github.com/swift-server/webauthn-swift.git", branch: "main")
16+
```
17+
18+
and `WebAuthn` dependency to your target:
19+
20+
```swift
21+
.target(name: "MyApp", dependencies: [.product(name: "WebAuthn", package: "webauthn-swift")])
22+
```
23+
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+
### Setup
49+
50+
Configure your backend with a `WebAuthnManager` instance:
51+
52+
```swift
53+
app.webAuthn = WebAuthnManager(
54+
config: WebAuthnConfig(
55+
relyingPartyDisplayName: "My Fancy Web App",
56+
relyingPartyID: "example.com",
57+
relyingPartyOrigin: "https://example.com",
58+
timeout: 600
59+
)
60+
)
61+
```
62+
63+
### Registration
64+
65+
Scenario: A user wants to signup on a website using WebAuthn.
66+
67+
#### Explanation
68+
69+
1. When tapping the "Register" button the client sends a request to
70+
the backend. The backend responds to this request with a call to `begingRegistration(user:)` which then returns a
71+
new `PublicKeyCredentialRequestOptions`. This must be send back to the client so it can pass it to
72+
`navigator.credentials.create()`.
73+
74+
2. Whatever `navigator.credentials.create()` returns will be send back to the backend, parsing it into
75+
`RegistrationCredential`.
76+
```swift
77+
let registrationCredential = try req.content.decode(RegistrationCredential.self)
78+
```
79+
80+
3. Next the backend calls `finishRegistration(challenge:credentialCreationData:)` with the previously
81+
generated challenge and the received `RegistrationCredential`. If `finishRegistration` succeeds a new `Credential`
82+
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.
83+
84+
##### Example implementation (using Vapor)
85+
86+
```swift
87+
authSessionRoutes.get("makeCredential") { req -> PublicKeyCredentialCreationOptions in
88+
let user = try req.auth.require(User.self)
89+
let options = try req.webAuthn.beginRegistration(user: user)
90+
req.session.data["challenge"] = options.challenge
91+
return options
92+
}
93+
94+
authSessionRoutes.post("makeCredential") { req -> HTTPStatus in
95+
let user = try req.auth.require(User.self)
96+
guard let challenge = req.session.data["challenge"] else { throw Abort(.unauthorized) }
97+
let registrationCredential = try req.content.decode(RegistrationCredential.self)
98+
99+
let credential = try await req.webAuthn.finishRegistration(
100+
challenge: challenge,
101+
credentialCreationData: registrationCredential,
102+
// this is likely to be removed soon
103+
confirmCredentialIDNotRegisteredYet: { credentialID in
104+
try await queryCredentialWithUser(id: credentialID) == nil
105+
}
106+
)
107+
108+
try await WebAuthnCredential(from: credential, userID: user.requireID())
109+
.save(on: req.db)
110+
111+
return .ok
112+
}
113+
```
114+
115+
### Authentication
116+
117+
Scenario: A user wants to log in on a website using WebAuthn.
118+
119+
#### Explanation
120+
121+
1. When tapping the "Login" button the client sends a request to
122+
the backend. The backend responds to this request with a call to `beginAuthentication()` which then in turn
123+
returns a new `PublicKeyCredentialRequestOptions`. This must be sent back to the client so it can pass it to
124+
`navigator.credentials.get()`.
125+
2. Whatever `navigator.credentials.get()` returns will be sent back to the backend, parsing it into
126+
`AuthenticationCredential`.
127+
```swift
128+
let authenticationCredential = try req.content.decode(AuthenticationCredential.self)
129+
```
130+
3. Next the backend calls
131+
`finishAuthentication(credential:expectedChallenge:credentialPublicKey:credentialCurrentSignCount:)`.
132+
- The `credential` parameter expects the decoded `AuthenticationCredential`
133+
- The `expectedChallenge` parameter expects the challenge previously generated
134+
from `beginAuthentication()` (obtained e.g. through a session).
135+
- Query the persisted credential from [Registration](#registration) using the credential id from the decoded
136+
`AuthenticationCredential`. Pass this credential in the `credentialPublicKey` parameter and it's sign count to
137+
`credentialCurrentSignCount`.
138+
139+
4. If `finishAuthentication` succeeds you can safely login the user linked to the credential! `finishAuthentication`
140+
will return a `VerifiedAuthentication` with the updated sign count and a few other pieces of information to be
141+
persisted. Use this to update the credential in the database.
142+
143+
#### Example implementation
144+
145+
```swift
146+
// this endpoint will be called on clicking "Login"
147+
authSessionRoutes.get("authenticate") { req -> PublicKeyCredentialRequestOptions in
148+
let options = try req.webAuthn.beginAuthentication()
149+
req.session.data["challenge"] = String.base64URL(fromBase64: options.challenge)
150+
return options
151+
}
152+
153+
// this endpoint will be called after the user used e.g. TouchID.
154+
authSessionRoutes.post("authenticate") { req -> HTTPStatus in
155+
guard let challenge = req.session.data["challenge"] else { throw Abort(.unauthorized) }
156+
let data = try req.content.decode(AuthenticationCredential.self)
157+
guard let credential = try await queryCredentialWithUser(id: data.id) else {
158+
throw Abort(.unauthorized)
159+
}
160+
161+
let verifiedAuthentication = try req.webAuthn.finishAuthentication(
162+
credential: data,
163+
expectedChallenge: challenge,
164+
credentialPublicKey: [UInt8](credential.publicKey.base64URLDecodedData!),
165+
credentialCurrentSignCount: 0
166+
)
167+
168+
req.auth.login(credential.user)
169+
170+
return .ok
171+
}
172+
```
173+
174+
## Credits
175+
176+
Swift WebAuthn is heavily inspired by existing WebAuthn libraries like
177+
[py_webauthn](https://github.com/duo-labs/py_webauthn) and [go-webauthn](https://github.com/go-webauthn/webauthn).
178+
179+
## Links
180+
181+
- [WebAuthn.io](https://webauthn.io/)
182+
- [WebAuthn guide](https://webauthn.guide/)
183+
- [WebAuthn Spec](https://w3c.github.io/webauthn/)
184+
- [CBOR.me](https://cbor.me/)

0 commit comments

Comments
 (0)