Skip to content

Commit 1086247

Browse files
authored
Rename code for SnapAuth branding, add README (#21)
This updates the package name and default host, and adds a reasonably-detailed README. It also exports a couple more types to provide more flexible client data structures.
1 parent 9f5b992 commit 1086247

File tree

5 files changed

+184
-9
lines changed

5 files changed

+184
-9
lines changed

README.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# SnapAuth TypeScript/JavaScript SDK
2+
3+
The official TS/JS SDK for SnapAuth 🫰
4+
5+
## Installation and Setup
6+
### Node
7+
```bash
8+
npm i --save @snapauth/sdk
9+
```
10+
or
11+
```bash
12+
yarn add @snapauth/sdk
13+
```
14+
15+
```typescript
16+
import { SDK } from '@snapauth/sdk'
17+
const snapAuth = new SDK('pubkey_your_value')
18+
```
19+
20+
### Directly linking (UMD)
21+
```html
22+
<script src="https://unpkg.com/@snapauth/[email protected]/dist/index.js"></script>
23+
<script type="text/javascript">
24+
const snapAuth = new SnapAuth.SDK('pubkey_your_value')
25+
</script>
26+
```
27+
28+
## Usage
29+
All examples are in TypeScript.
30+
For use with vanilla JavaScript, omit the type imports and annotations.
31+
32+
> [!IMPORTANT]
33+
> Both registration and authentication MUST be called in response to a user gesture.
34+
> Browsers will block the attempt otherwise.
35+
> This includes `onClick`, `onSubmit`, etc.
36+
37+
### Registering a Credential
38+
39+
```typescript
40+
// Get `handle` and `name` from fields in your UI.
41+
// You MAY use the value of handle for name, but MUST explicitly do so.
42+
const registration = await snapAuth.startRegister({ handle, name })
43+
if (registration.ok) {
44+
const token = registration.data.token
45+
// Send token to your backend to use the /registration/attach API
46+
} else {
47+
// Inspect registration.error and decide how best to proceed
48+
}
49+
```
50+
51+
> [!NOTE]
52+
> Registration requires you to provide either:
53+
>
54+
> `id`: A stable user identifier (e.g. primary key), or
55+
>
56+
> `handle`: A possibly-unstable identifier - what the user would type to sign in
57+
>
58+
> You may provide both now, and MUST provide both in the backend `attach` API call.
59+
>
60+
> You MUST also provide `name`, which is what the user sees during authentication.
61+
> It is used completelly locally, and not even sent to SnapAuth's servers.
62+
> This is commonly something like a human name, email address, or login handle.
63+
>
64+
> You MAY also set `displayName`.
65+
> If not provided, we default it to the `name` value.
66+
> Browsers typically (counter-intuitively) ignore `displayName` in favor of `name`.
67+
>
68+
> This is reflected in the TypeScript formats.
69+
70+
> [!CAUTION]
71+
> You MUST send the token to the backend `/registration/attach` API to associate it with the user.
72+
> Failure to do so will prevent the user from signing in the credential they just created.
73+
> The response also includes a `expiresAt` field containing a Unix timestamp indicating by when the token must be attached.
74+
75+
> [!WARNING]
76+
> The `name` field cannot be changed at this time - it's not supported by browers.
77+
> Once browser APIs exist to modify it, we will add support to the SDK.
78+
79+
80+
### Authenticating
81+
82+
```typescript
83+
// This would typically be in an onClick/onSubmit handler
84+
const handle = document.getElementById('username').value // Adjust to your UI
85+
const auth = await snapAuth.startAuth({ handle })
86+
if (auth.ok) {
87+
const token = auth.data.token
88+
// Send token to your backend to use the /auth/verify API
89+
// It will return the verified user's id and handle, which you should use to
90+
// sign in the user with your existing mechanism (cookie, token, etc)
91+
} else {
92+
// Inspect auth.error and decide how best to proceed
93+
}
94+
```
95+
96+
> [!TIP]
97+
> You may use `id` or `handle` when calling `startAuth()`.
98+
> Using `id` will typically require a roundtrip to your service, but tends to be necessary if you normalize handles.
99+
> Both values are **case-insensitive**.
100+
101+
> [!CAUTION]
102+
> DO NOT sign in the user based on getting the client token alone!
103+
> You MUST send it to the `/auth/verify` endpoint, and inspect its response to get the _verified_ user id to securely authenticate.
104+
105+
#### AutoFill-assisted requests
106+
107+
Most browsers support credential autofill, which will automatically prompt a user to sign in using a previous-registered credential.
108+
To take advantage of this, you need two things:
109+
110+
1) An `<input>` (or `<textarea>`) field with `autocomplete="username webauthn"` set[^1].
111+
We strongly recommend adding these details to your standard sign-in field:
112+
```html
113+
<input type="text" autocomplete="username webauthn" placeholder="Username" />
114+
```
115+
116+
2) Run the `handleAutofill` API. This takes a callback which runs on successful authentication using the autofill API:
117+
```typescript
118+
// Type import is optional, but recommended.
119+
import { AuthResponse } from '@snapauth/sdk'
120+
const onSignIn = (auth: AuthResponse) => {
121+
if (auth.ok) {
122+
// send `auth.data.token` to your backend, as above
123+
}
124+
}
125+
snapAuth.handleAutofill(onSignIn)
126+
```
127+
128+
> [!IMPORTANT]
129+
> Never rely on the autofill experience alone.
130+
> Always treat it as progressive enhancement to the standard flow.
131+
132+
> [!TIP]
133+
> Re-use the `handleAutofill` callback in the traditional flow to create a consistent experience:
134+
```typescript
135+
const validateAuth = async (auth: AuthResponse) => {
136+
if (auth.ok) {
137+
await fetch(...) // send auth.data.token
138+
}
139+
}
140+
const onSignInSubmit = async (e) => {
141+
// ...
142+
const auth = await snapAuth.startAuth({ handle })
143+
await validateAuth(auth)
144+
}
145+
sdk.handleAutofill(validateAuth)
146+
```
147+
148+
> [!TIP]
149+
> Unlike the direct startRegister and startAuth calls, handleAutofill CAN and SHOULD be called as early in the page lifecycle is possible.
150+
> This helps ensure that autofill can occur when a user interacts with the form field.
151+
152+
## Building the SDK
153+
154+
Run `npm run watch` to keep the build running continually on file change.
155+
156+
To make the local version available for linking, run `npm link` in this directory.
157+
158+
In the project that should _use_ the local version, run `npm link '@snapauth/sdk'` which will set up the symlinking.
159+
160+
If working with a non-production backend, provide the host as a string to the second parameter of the SDK constructor.
161+
162+
[^1]: The WebAuthn spec says that only `webauthn` is required in `autocomplete`, but real-world browser testing shows that using exactly `autocomplete="username webauthn"` string is most reliable.
163+
If you do not have this element, or the browser otherwise fails to detect it, the autofill-assited experience will not start.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"name": "@webauthnbiz/sdk",
3-
"version": "0.0.7",
4-
"description": "My Awesome SDK",
2+
"name": "@snapauth/sdk",
3+
"version": "0.1.0",
4+
"description": "SnapAuth JS/TS SDK",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
77
"scripts": {

src/SDK.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type UserIdOrHandle =
3838
| { handle: string }
3939
type OptionalUserIdOrHandle = UserIdOrHandle | undefined
4040

41+
export type UserAuthenticationInfo = UserIdOrHandle
4142
export type UserRegistrationInfo = {
4243
name: string
4344
displayName?: string
@@ -47,7 +48,7 @@ class SDK {
4748
private apiKey: string
4849
private host: string
4950

50-
constructor(publicKey: string, host: string = 'https://api.webauthn.biz') {
51+
constructor(publicKey: string, host: string = 'https://api.snapauth.app') {
5152
this.apiKey = publicKey
5253
this.host = host
5354
}
@@ -62,7 +63,7 @@ class SDK {
6263
// }
6364
}
6465

65-
async startAuth(user: UserIdOrHandle): Promise<AuthResponse> {
66+
async startAuth(user: UserAuthenticationInfo): Promise<AuthResponse> {
6667
this.requireWebAuthn()
6768
const res = await this.api('/auth/createOptions', { user }) as Result<CredentialRequestOptionsJSON, WebAuthnError>
6869
if (!res.ok) {
@@ -113,7 +114,13 @@ class SDK {
113114
return
114115
}
115116
const options = parseRequestOptions(res.data)
116-
callback(await this.doAuth(options, undefined))
117+
const response = await this.doAuth(options, undefined)
118+
if (response.ok) {
119+
callback(response)
120+
} else {
121+
// User aborted conditional mediation (UI doesn't even exist in all
122+
// browsers). Do not run the callback.
123+
}
117124
}
118125

119126
private async doAuth(options: CredentialRequestOptions, user: UserIdOrHandle|undefined): Promise<AuthResponse> {

src/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
1-
export { default as sdk } from './SDK'
2-
export { AuthResponse, RegisterResponse } from './SDK'
1+
export { default as SDK } from './SDK'
2+
export {
3+
AuthResponse,
4+
RegisterResponse,
5+
UserAuthenticationInfo,
6+
UserRegistrationInfo,
7+
} from './SDK'

webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module.exports = {
77
output: {
88
filename: 'index.js',
99
path: path.resolve(__dirname, 'dist'),
10-
library: 'WebAuthnBiz',
10+
library: 'SnapAuth',
1111
libraryTarget: 'umd',
1212
},
1313
module: {

0 commit comments

Comments
 (0)