|
| 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. |
0 commit comments