├── .eslintignore ├── .eslintrc.cjs ├── .github ├── FUNDING.yml └── workflows │ └── publish.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── malta.config.json └── pages │ ├── index.md │ └── reference │ ├── cookie │ ├── Cookie │ │ ├── index.md │ │ └── serialize.md │ ├── CookieAttributes.md │ ├── CookieControlller │ │ ├── createBlankCookie.md │ │ ├── createCookie.md │ │ ├── index.md │ │ └── parse.md │ ├── index.md │ ├── parseCookies.md │ └── serializeCookie.md │ ├── crypto │ ├── ECDSA │ │ ├── generateKeyPair.md │ │ ├── index.md │ │ ├── sign.md │ │ └── verify.md │ ├── HMAC │ │ ├── generateKey.md │ │ ├── index.md │ │ ├── sign.md │ │ └── verify.md │ ├── KeyPair.md │ ├── RSASSAPKCS1v1_5 │ │ ├── generateKeyPair.md │ │ ├── index.md │ │ ├── sign.md │ │ └── verify.md │ ├── RSASSAPSS │ │ ├── generateKeyPair.md │ │ ├── index.md │ │ ├── sign.md │ │ └── verify.md │ ├── SHAHash.md │ ├── alphabet.md │ ├── constantTimeEqual.md │ ├── generateRandomInteger.md │ ├── generateRandomString.md │ ├── index.md │ ├── random.md │ ├── sha1.md │ ├── sha256.md │ ├── sha384.md │ └── sha512.md │ ├── encoding │ ├── Base32Encoding │ │ ├── decode.md │ │ ├── encode.md │ │ └── index.md │ ├── Base64Encoding │ │ ├── decode.md │ │ ├── encode.md │ │ └── index.md │ ├── base32.md │ ├── base32hex.md │ ├── base64.md │ ├── base64url.md │ ├── decodeBase32.md │ ├── decodeBase64.md │ ├── decodeBase64url.md │ ├── decodeHex.md │ ├── encodeBase32.md │ ├── encodeBase64.md │ ├── encodeBase64url.md │ ├── encodeHex.md │ └── index.md │ ├── jwt │ ├── JWT.md │ ├── JWTAlgorithm.md │ ├── createJWT.md │ ├── index.md │ ├── parseJWT.md │ └── validateJWT.md │ ├── main │ ├── TimeSpan │ │ ├── index.md │ │ ├── milliseconds.md │ │ └── seconds.md │ ├── TimeSpanUnit.md │ ├── createDate.md │ ├── index.md │ └── isWithinExpirationDate.md │ ├── oauth2 │ ├── OAuth2Client │ │ ├── createAuthorizationURL.md │ │ ├── index.md │ │ ├── refreshAccessToken.md │ │ └── validateAuthorizationCode.md │ ├── OAuth2RequestError.md │ ├── TokenResponseBody.md │ ├── generateCodeVerifier.md │ ├── generateState.md │ └── index.md │ ├── otp │ ├── TOTPController │ │ ├── generate.md │ │ ├── index.md │ │ └── verify.md │ ├── createHOTPKeyURI.md │ ├── createTOTPKeyURI.md │ ├── generateHOTP.md │ └── index.md │ ├── password │ ├── Argon2id │ │ ├── hash.md │ │ ├── index.md │ │ └── verify.md │ ├── Bcrypt │ │ ├── hash.md │ │ ├── index.md │ │ └── verify.md │ ├── PasswordHashingAlgorithm.md │ ├── Scrypt │ │ ├── hash.md │ │ ├── index.md │ │ └── verify.md │ └── index.md │ ├── request │ ├── index.md │ └── verifyRequestOrigin.md │ └── webauthn │ ├── AssertionResponse.md │ ├── AttestationResponse.md │ ├── WebAuthnController │ ├── index.md │ ├── validateAssertionResponse.md │ └── validateAttestationResponse.md │ └── index.md ├── package.json ├── src ├── bytes.test.ts ├── bytes.ts ├── cookie │ ├── index.test.ts │ └── index.ts ├── crypto │ ├── buffer.ts │ ├── ecdsa.test.ts │ ├── ecdsa.ts │ ├── hmac.test.ts │ ├── hmac.ts │ ├── index.ts │ ├── random.test.ts │ ├── random.ts │ ├── rsa.test.ts │ ├── rsa.ts │ ├── sha.test.ts │ └── sha.ts ├── encoding │ ├── base32.test.ts │ ├── base32.ts │ ├── base64.test.ts │ ├── base64.ts │ ├── hex.test.ts │ ├── hex.ts │ └── index.ts ├── index.ts ├── jwt │ ├── index.test.ts │ └── index.ts ├── oauth2 │ └── index.ts ├── otp │ ├── hotp.test.ts │ ├── hotp.ts │ ├── index.ts │ ├── totp.ts │ └── uri.ts ├── password │ ├── argon2id.ts │ ├── bcrypt.ts │ ├── index.test.ts │ ├── index.ts │ └── scrypt.ts ├── request │ ├── index.test.ts │ └── index.ts └── webauthn │ └── index.ts ├── tsconfig.build.json ├── tsconfig.json ├── vitest.config.ts └── vitest.setup.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/** -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | "@typescript-eslint/no-explicit-any": "off", 4 | "@typescript-eslint/no-empty-function": "off", 5 | "@typescript-eslint/ban-types": "off", 6 | "@typescript-eslint/no-unused-vars": [ 7 | "error", 8 | { 9 | argsIgnorePattern: "^_", 10 | varsIgnorePattern: "^_", 11 | caughtErrorsIgnorePattern: "^_" 12 | } 13 | ], 14 | "@typescript-eslint/no-empty-interface": "off", 15 | "@typescript-eslint/explicit-function-return-type": "error", 16 | "no-async-promise-executor": "off", 17 | "no-useless-catch": "off" 18 | }, 19 | parser: "@typescript-eslint/parser", 20 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 21 | plugins: ["@typescript-eslint"], 22 | env: { 23 | node: true 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: pilcrowOnPaper 2 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: "Publish package and docs" 2 | on: [push] 3 | 4 | permissions: 5 | contents: write 6 | pages: write 7 | id-token: write 8 | 9 | env: 10 | AURI_GITHUB_TOKEN: ${{secrets.AURI_GITHUB_TOKEN}} 11 | NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}} 12 | 13 | jobs: 14 | publish-package: 15 | name: Publish package 16 | runs-on: ubuntu-latest 17 | outputs: 18 | changesets: ${{steps.check-changesets.outputs.changesets}} 19 | steps: 20 | - name: Setup actions 21 | uses: actions/checkout@v3 22 | with: 23 | ref: ${{ github.ref }} 24 | - name: Setup Node 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: 20 28 | registry-url: "https://registry.npmjs.org/" 29 | - name: Install Auri 30 | run: npm install -g auri@1.0.2 31 | - name: Prepare release 32 | run: npx auri prepare ${{ github.ref_name }} 33 | - name: Publish package 34 | run: npx auri publish ${{ github.ref_name }} 35 | 36 | check-changesets: 37 | name: Check changesets 38 | needs: publish-package 39 | runs-on: ubuntu-latest 40 | outputs: 41 | changesets: ${{steps.check-changesets.outputs.changesets}} 42 | steps: 43 | - name: Check if ".changesets" directory has files 44 | id: check-changesets 45 | run: | 46 | if [ -z "$(ls -A .changesets)" ]; then 47 | echo "changesets=0" >> "$GITHUB_OUTPUT" 48 | else 49 | echo "changesets=1" >> "$GITHUB_OUTPUT" 50 | fi 51 | 52 | publish-docs: 53 | name: Publish docs 54 | needs: check-changesets 55 | if: needs.check-changesets.check-changesets.outputs.changesets == 0 && github.ref_name == 'main' 56 | runs-on: ubuntu-latest 57 | environment: 58 | name: github-pages 59 | url: ${{ steps.deployment.outputs.page_url }} 60 | steps: 61 | - name: Set up actions 62 | uses: actions/checkout@v3 63 | - name: Install malta 64 | working-directory: docs 65 | run: | 66 | curl -o malta.tgz -L https://github.com/pilcrowonpaper/malta/releases/latest/download/linux-amd64.tgz 67 | tar -xvzf malta.tgz 68 | - name: build 69 | working-directory: docs 70 | run: ./linux-amd64/malta build 71 | - name: Upload pages artifact 72 | uses: actions/upload-pages-artifact@v1 73 | with: 74 | path: "docs/dist" 75 | - name: Deploy to GitHub Pages 76 | id: deployment 77 | uses: actions/deploy-pages@v1 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | dist 3 | node_modules 4 | package-lock.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | pnpm-lock.yaml 6 | package-lock.json 7 | yarn.lock 8 | 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "trailingComma": "none", 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # oslo 2 | 3 | ## 1.2.1 4 | 5 | ### Patch changes 6 | 7 | - Fix `issuer` parameter getting encoded twice in key URI ([#79](https://github.com/pilcrowOnPaper/oslo/pull/79)) 8 | 9 | ## 1.2.0 10 | 11 | ### Minor changes 12 | 13 | - Feat: Add `codeChallengeMethod` option to `OAuth2Client.createAuthorizationURL()` ([#29](https://github.com/pilcrowOnPaper/oslo/pull/29)) 14 | 15 | ## 1.1.3 16 | 17 | ### Patch changes 18 | 19 | - Update dependencies. ([#51](https://github.com/pilcrowOnPaper/oslo/pull/51)) 20 | 21 | ## 1.1.2 22 | 23 | ### Patch changes 24 | 25 | - Export OAuth2 token response interface ([#45](https://github.com/pilcrowOnPaper/oslo/pull/45)) 26 | - Fix client data JSON validation 27 | 28 | ## 1.1.1 29 | 30 | ### Patch changes 31 | 32 | - Improve `base64.encode()` performance 33 | - Improve `base32.encode()` performance. ([#40](https://github.com/pilcrowOnPaper/oslo/pull/40)) 34 | 35 | ## 1.1.0 36 | 37 | ### Minor changes 38 | 39 | - Deprecate `encodeBase32()`, `decodeBase32()`, `encodeBase64`, `decodeBase64()`, `encodeBase64url()`, `decodeBase64url()`. ([#35](https://github.com/pilcrowOnPaper/oslo/pull/35)) 40 | - Feat: Add `Base64Encoding`, `Base32Encoding`, `base16`, `base32`, `base32hex`, `base64`, `base64url`. ([#35](https://github.com/pilcrowOnPaper/oslo/pull/35)) 41 | 42 | ## 1.0.4 43 | 44 | ### Patch changes 45 | 46 | - Fix: Export `TimeSpanUnit` ([#30](https://github.com/pilcrowOnPaper/oslo/pull/30)) 47 | 48 | ## 1.0.3 49 | 50 | - Pin `@node-rs/argon2` and `@node-rs/bcrypt` versions [#26](https://github.com/pilcrowOnPaper/oslo/pull/26) 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor manual 2 | 3 | ## Contributing to the docs 4 | 5 | We welcome all contributions to the docs, especially grammar fixes. Oslo uses [Malta](https://malta-cli.pages.dev) for generating documentation sites. All pages are markdown files located in the `docs/pages` directory. Make sure to update `malta.config.json` if you need a page to appear in the sidebar. 6 | 7 | ## Contributing to the source code 8 | 9 | We are open to most contributions, but please open a new issue before creating a pull request, especially for new features. It's likely your PR will be rejected if not. We have intentionally limited the scope of the project and we would like to keep the package lean. 10 | 11 | ### Set up 12 | 13 | Install dependencies with PNPM. 14 | 15 | ``` 16 | pnpm i 17 | ``` 18 | 19 | ### Testing 20 | 21 | Run `pnpm test` to run tests and `pnpm build` to build the package. 22 | 23 | ``` 24 | pnpm test 25 | 26 | pnpm build 27 | ``` 28 | 29 | ### Creating changesets 30 | 31 | When creating a PR, create a changeset with `pnpm auri add`. If you made multiple changes, create multiple changesets. Use `minor` for new features, and use `patch` for bug fixes: 32 | 33 | ``` 34 | pnpm auri add minor 35 | pnpm auri add patch 36 | ``` 37 | 38 | A new markdown file should be created in `.changesets` directory. Write a short summary of the change: 39 | 40 | ``` 41 | Fix: Handle negative numbers in `sqrt()` 42 | ``` 43 | 44 | ``` 45 | Feat: Add `greet()` 46 | ``` 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 pilcrowOnPaper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `oslo` 2 | 3 | **This package has been deprecated. Please use the packages published under the [Oslo project](https://oslojs.dev) instead.** 4 | 5 | A collection of auth-related utilities, including: 6 | 7 | - `oslo/cookie`: Cookie parsing and serialization 8 | - `oslo/crypto`: Generate hashes, signatures, and random values 9 | - `oslo/encoding`: Encode base64, base64url, base32, hex 10 | - `oslo/jwt`: Create and verify JWTs 11 | - `oslo/oauth2`: OAuth2 helpers 12 | - `oslo/otp`: HOTP, TOTP 13 | - `oslo/password`: Password hashing 14 | - `oslo/request`: CSRF protection 15 | - `oslo/webauthn`: Verify Web Authentication API attestations and assertions 16 | 17 | Aside from `oslo/password`, every module works in any environment, including Node.js, Cloudflare Workers, Deno, and Bun. 18 | 19 | Documentation: https://oslo.js.org 20 | 21 | ## Installation 22 | 23 | ``` 24 | npm i oslo 25 | pnpm add oslo 26 | yarn add oslo 27 | ``` 28 | 29 | ## Node.js 30 | 31 | For Node.js 16 & 18, you need to polyfill the Web Crypto API. This is not required in Node.js 20. 32 | 33 | ```ts 34 | import { webcrypto } from "node:crypto"; 35 | 36 | globalThis.crypto = webcrypto; 37 | ``` 38 | 39 | Alternatively, add the `--experimental-global-webcrypto` flag when running Node. 40 | 41 | ``` 42 | node --experimental-global-webcrypto index.js 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/malta.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Oslo", 3 | "description": "A collection of auth utilities.", 4 | "domain": "https://oslo.js.org", 5 | "twitter": "@pilcrowonpaper", 6 | "sidebar": [ 7 | { 8 | "title": "API reference", 9 | "pages": [ 10 | ["oslo", "/reference/main"], 11 | ["oslo/cookie", "/reference/cookie"], 12 | ["oslo/crypto", "/reference/crypto"], 13 | ["oslo/encoding", "/reference/encoding"], 14 | ["oslo/jwt", "/reference/jwt"], 15 | ["oslo/oauth2", "/reference/oauth2"], 16 | ["oslo/otp", "/reference/otp"], 17 | ["oslo/password", "/reference/password"], 18 | ["oslo/request", "/reference/request"], 19 | ["oslo/webauthn", "/reference/webauthn"] 20 | ] 21 | }, 22 | { 23 | "title": "Links", 24 | "pages": [ 25 | ["GitHub", "https://github.com/pilcrowonpaper/oslo"], 26 | ["Twitter", "https://twitter.com/pilcrowonpaper"], 27 | ["Donate", "https://github.com/sponsors/pilcrowOnPaper"] 28 | ] 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /docs/pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Oslo documentation" 3 | --- 4 | 5 | # Oslo documentation 6 | 7 | **This package has been deprecated. Please use the packages published under the [Oslo project](https://oslojs.dev) instead.** 8 | 9 | Oslo provides a bunch of auth utilities, including APIs for: 10 | 11 | - Creating JWTs 12 | - OAuth 2.0 clients 13 | - Hashing passwords 14 | - Generating cryptographically strong random values 15 | - Signing and encoding data 16 | - Verifying WebAuthn responses 17 | - Zero dependencies (except for `oslo/password`) 18 | 19 | It's lightweight, runtime agnostic, and fully typed. 20 | 21 | ## Installation 22 | 23 | ``` 24 | npm install oslo 25 | ``` 26 | 27 | This module relies on the Web Crypto API, which is not available by default in Node.js 16 and 18. Make sure to polyfill them: 28 | 29 | ```ts 30 | import { webcrypto } from "node:crypto"; 31 | 32 | globalThis.crypto = webcrypto; 33 | ``` 34 | 35 | Or alternatively, enable the flag when running Node.js: 36 | 37 | ``` 38 | node --experimental-global-webcrypto index.js 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/pages/reference/cookie/Cookie/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Cookie" 3 | --- 4 | 5 | # `Cookie` 6 | 7 | Represents a cookie. 8 | 9 | ## Constructor 10 | 11 | ```ts 12 | //$ CookieAttributes=/reference/cookie/CookieAttributes 13 | function constructor(name: string, value: string, attributes?: $$CookieAttributes): this; 14 | ``` 15 | 16 | ### Parameters 17 | 18 | - `name` 19 | - `value` 20 | - `attributes` 21 | 22 | ## Methods 23 | 24 | - [`serialize()`](/reference/cookie/Cookie/serialize) 25 | 26 | ## Properties 27 | 28 | ```ts 29 | //$ CookieAttributes=/reference/cookie/CookieAttributes 30 | interface Properties { 31 | name: string; 32 | value: string; 33 | attributes: $$CookieAttributes; 34 | } 35 | ``` 36 | 37 | - `name` 38 | - `value` 39 | - `attributes` 40 | 41 | ## Example 42 | 43 | ```ts 44 | import { Cookie } from "oslo/cookie"; 45 | 46 | const sessionCookie = new Cookie("session", sessionId, { 47 | maxAge: 60 * 60 * 24, 48 | httpOnly: true, 49 | secure: true, 50 | path: "/" 51 | }); 52 | response.headers.set("Set-Cookie", sessionCookie.serialize()); 53 | ``` 54 | 55 | If your framework provides an API for setting cookies: 56 | 57 | ```ts 58 | import { Cookie } from "oslo/cookie"; 59 | 60 | const sessionCookie = new Cookie("session", sessionId, { 61 | maxAge: 60 * 60 * 24, 62 | httpOnly: true, 63 | secure: true, 64 | path: "/" 65 | }); 66 | setCookie(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/pages/reference/cookie/Cookie/serialize.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Cookie.serialize()" 3 | --- 4 | 5 | # `Cookie.serialize()` 6 | 7 | Serializes cookie for `Set-Cookie` header. 8 | 9 | ```ts 10 | function serialize(): string; 11 | ``` 12 | 13 | ## Example 14 | 15 | ```ts 16 | response.headers.set("Set-Cookie", cookie.serialize()); 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/pages/reference/cookie/CookieAttributes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "CookieAttributes" 3 | --- 4 | 5 | # `CookieAttributes` 6 | 7 | Cookie attributes for `Set-Cookie` header. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | interface CookieAttributes { 13 | secure?: boolean; 14 | path?: string; 15 | domain?: string; 16 | sameSite?: "lax" | "strict" | "none"; 17 | httpOnly?: boolean; 18 | maxAge?: number; 19 | expires?: Date; 20 | } 21 | ``` 22 | 23 | ### Properties 24 | 25 | - `secure` 26 | - `path` 27 | - `domain` 28 | - `sameSite` 29 | - `httpOnly` 30 | - `maxAge`: `Max-Age` (seconds) 31 | - `expires` 32 | 33 | ## Example 34 | 35 | ```ts 36 | // Secure; Path=/; Domain=example.com; SameSite=Lax; HttpOnly; Max-Age: 3600; Expires=Thu, 01 Jan 1970 00:00:00 GMT 37 | const attributes: CookieAttributes = { 38 | secure: true, 39 | path: "/", 40 | domain: "example.com", 41 | sameSite: "lax", 42 | httpOnly: true, 43 | maxAge: 60 * 60, 44 | expires: new Date() 45 | }; 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/pages/reference/cookie/CookieControlller/createBlankCookie.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "CookieController.createBlankCookie()" 3 | --- 4 | 5 | # `CookieController.createBlankCookie()` 6 | 7 | Creates a new cookie that deletes an existing cookie. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ Cookie=/reference/cookie/Cookie 13 | function createBlankCookie(): $$Cookie; 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/pages/reference/cookie/CookieControlller/createCookie.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "CookieController.createCookie()" 3 | --- 4 | 5 | # `CookieController.createCookie()` 6 | 7 | Creates a new cookie. Use [`CookieController.createBlankCookie()`](/reference/cookie/CookieController/createBlankCookie) to remove the cookie. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ Cookie=/reference/cookie/Cookie 13 | function createCookie(value: string): $$Cookie; 14 | ``` 15 | 16 | ### Parameters 17 | 18 | - `value` 19 | -------------------------------------------------------------------------------- /docs/pages/reference/cookie/CookieControlller/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "CookieController" 3 | --- 4 | 5 | # `CookieController` 6 | 7 | Provides methods for handling cookies. 8 | 9 | ## Constructor 10 | 11 | ```ts 12 | //$ CookieAttributes=/reference/cookie/CookieAttributes 13 | //$ TimeSpan=/reference/cookie/TimeSpan 14 | function constructor( 15 | cookieName: string, 16 | baseCookieAttributes: $$CookieAttributes, 17 | cookieOptions?: { 18 | expiresIn?: $$TimeSpan; 19 | } 20 | ): this; 21 | ``` 22 | 23 | ### Parameters 24 | 25 | - `cookieName` 26 | - `baseCookieAttributes`: `expires` and `maxAge` will be overridden 27 | - `cookieOptions` 28 | - `expiresIn` 29 | 30 | ## Methods 31 | 32 | - [`createCookie()`](/reference/session/CookieController) 33 | - [`createBlankCookie()`](/reference/session/CookieController) 34 | - [`parse()`](/reference/session/CookieController) 35 | 36 | ## Properties 37 | 38 | ```ts 39 | interface Properties { 40 | cookieName: string; 41 | } 42 | ``` 43 | 44 | - `cookieName` 45 | 46 | ## Example 47 | 48 | ```ts 49 | //$ TimeSpan=/reference/main/TimeSpan 50 | import { SessionCookieController } from "oslo/session"; 51 | import { $$TimeSpan } from "oslo"; 52 | 53 | import type { CookieAttributes } from "oslo/cookie"; 54 | 55 | const baseSessionCookieAttributes: CookieAttributes = { 56 | httpOnly: true 57 | }; 58 | const sessionCookieController = new CookieController(cookieName, baseSessionCookieAttributes, { 59 | expiresIn: new TimeSpan(30, "d") 60 | }); 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/pages/reference/cookie/CookieControlller/parse.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "CookieController.parse()" 3 | --- 4 | 5 | # `CookieController.parse()` 6 | 7 | Parses a `Cookie` header and returns the session cookie value. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function parse(header: string): string | null; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `header`: `Cookie` header 18 | 19 | ## Example 20 | 21 | ```ts 22 | //$ TimeSpan=/reference/main/TimeSpan 23 | import { $$TimeSpan } from "oslo"; 24 | 25 | const cookieName = "session"; 26 | const controller = new CookieController("session", {}); 27 | 28 | // "abc" 29 | const sessionId = controller.parseCookies("session=abc"); 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/pages/reference/cookie/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "oslo/cookie" 3 | --- 4 | 5 | # `oslo/cookie` 6 | 7 | Provides utilities for working with HTTP cookies. 8 | 9 | ## Functions 10 | 11 | - [`parseCookies()`](/reference/cookie/parseCookies) 12 | - [`serializeCookie()`](/reference/cookie/serializeCookie) 13 | 14 | ## Interfaces 15 | 16 | - [`CookieAttributes`](/reference/cookie/CookieAttributes) 17 | -------------------------------------------------------------------------------- /docs/pages/reference/cookie/parseCookies.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "parseCookies()" 3 | --- 4 | 5 | # `parseCookies()` 6 | 7 | Parses `Cookie` header value and returns a `Map` with the cookie names as the key. Cookie names and values are URI-component decoded. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function parseCookies(cookieHeader: string): Map; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `cookieHeader`: `Cookie` HTTP header 18 | 19 | ## Example 20 | 21 | ```ts 22 | import { parseCookies } from "oslo/cookie"; 23 | 24 | const cookies = parseCookies("message=hello"); 25 | const message = cookies.get("message"); 26 | ``` 27 | 28 | Cookie names and values are URI-component decoded when parsed. 29 | 30 | ```ts 31 | const cookies = parseCookies("json=%7B%22message%22%3A%22hello%22%7D"); 32 | const parsedJSON = JSON.parse(cookies.get("json")); 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/pages/reference/cookie/serializeCookie.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "serializeCookie()" 3 | --- 4 | 5 | # `serializeCookie()` 6 | 7 | Serializes cookie for `Set-Cookie` header. The cookie name and value are URI-component encoded. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ CookieAttributes=/reference/cookie/CookieAttributes 13 | function serializeCookie(name: string, value: string, attributes: $$CookieAttributes): string; 14 | ``` 15 | 16 | ### Parameters 17 | 18 | - `name` 19 | - `value` 20 | - `attributes` 21 | 22 | ## Example 23 | 24 | ```ts 25 | import { serializeCookie } from "oslo/cookie"; 26 | 27 | // // message=hello; Secure; Path=/; Domain=example.com; SameSite=Lax; HttpOnly; Max-Age: 3600; Expires=Thu, 01 Jan 1970 00:00:00 GMT 28 | const serialized = serializeCookie("message", "hello", { 29 | expires: new Date(), 30 | maxAge: 60 * 60, // 1 hour 31 | path: "/", 32 | httpOnly: true, 33 | secure: true, 34 | sameSite: "lax" 35 | }); 36 | response.headers.set("Set-Cookie", serialized); 37 | ``` 38 | 39 | The name and value is properly encoded, so you can pass any arbitrary string: 40 | 41 | ```ts 42 | serializeCookie("! *[~", "$(;:_"); 43 | serializeCookie( 44 | "json", 45 | JSON.stringify({ 46 | message: "hello" 47 | }) 48 | ); 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/ECDSA/generateKeyPair.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "ECDSA.generateKeyPair()" 3 | --- 4 | 5 | # `ECDSA.generateKeyPair()` 6 | 7 | Generates a new public/private key pair. The public key is in SPKI format and the private key is in PKCS#8 format. See [`ECDSA`](/reference/crypto/ECDSA) for an example. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ KeyPair=/reference/crypto/KeyPair 13 | function generateKeyPair(): Promise<$$KeyPair>; 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/ECDSA/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "ECDSA" 3 | --- 4 | 5 | # `ECDSA` 6 | 7 | Helper for ECDSA. 8 | 9 | ## Constructor 10 | 11 | ```ts 12 | //$ SHAHash=/reference/crypto/SHAHash 13 | function constructor(hash: $$SHAHash, curve: "P-256" | "P-384" | "P-521"): this; 14 | ``` 15 | 16 | ### Parameters 17 | 18 | - `hash` 19 | - `curve` 20 | 21 | ## Methods 22 | 23 | - [`generateKeyPair()`](/reference/crypto/ECDSA/generateKeyPair) 24 | - [`sign()`](/reference/crypto/ECDSA/sign) 25 | - [`verify()`](/reference/crypto/ECDSA/verify) 26 | 27 | ## Example 28 | 29 | ```ts 30 | import { ECDSA } from "oslo/crypto"; 31 | 32 | const es256 = new ECDSA("SHA-256", "P-256"); 33 | 34 | const { publicKey, privateKey } = await es256.generateKeyPair(); 35 | const data = new TextEncoder().encode("hello, world"); 36 | 37 | const signature = await es256.sign(privateKey, data); 38 | 39 | const validSignature = await es256.verify(publicKey, signature, data); 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/ECDSA/sign.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "ECDSA.sign()" 3 | --- 4 | 5 | # `ECDSA.sign()` 6 | 7 | Signs data with a private key and returns the signature. See [`ECDSA`](/reference/crypto/ECDSA) for an example. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function sign( 13 | privateKey: ArrayBuffer | TypedArray, 14 | data: ArrayBuffer | TypedArray 15 | ): Promise; 16 | ``` 17 | 18 | ### Parameters 19 | 20 | - `privateKey`: Private key in PKCS#8 format 21 | - `data`: Data to sign 22 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/ECDSA/verify.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "ECDSA.verify()" 3 | --- 4 | 5 | # `ECDSA.verify()` 6 | 7 | Verifies a signature with a public key and returns `true` if the signature is valid. See [`ECDSA`](/reference/crypto/ECDSA) for an example. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function sign( 13 | publicKey: ArrayBuffer | TypedArray, 14 | signature: ArrayBuffer | TypedArray, 15 | data: ArrayBuffer | TypedArray 16 | ): Promise; 17 | ``` 18 | 19 | ### Parameters 20 | 21 | - `publicKey`: Public key in SPKI format 22 | - `signature` 23 | - `data`: The original signed data 24 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/HMAC/generateKey.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "HMAC.generateKey()" 3 | --- 4 | 5 | # `HMAC.generateKey()` 6 | 7 | Generates a new secret key in raw format. See [`HMAC`](/reference/crypto/HMAC) for an example. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function generateKeyPair(): Promise; 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/HMAC/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "HMAC" 3 | --- 4 | 5 | # `HMAC` 6 | 7 | Helper for HMAC. 8 | 9 | ## Constructor 10 | 11 | ```ts 12 | //$ SHAHash=/reference/crypto/SHAHash 13 | function constructor(hash: $$SHAHash): this; 14 | ``` 15 | 16 | ### Parameters 17 | 18 | - `hash` 19 | 20 | ## Methods 21 | 22 | - [`generateKeyPair()`](/reference/crypto/HMAC/generateKeyPair) 23 | - [`sign()`](/reference/crypto/HMAC/sign) 24 | - [`verify()`](/reference/crypto/HMAC/verify) 25 | 26 | ## Example 27 | 28 | ```ts 29 | import { HMAC } from "oslo/crypto"; 30 | 31 | const hs256 = new HMAC("SHA-256"); 32 | 33 | const secretKey = await hs256.generateKeyPair(); 34 | const data = new TextEncoder().encode("hello, world"); 35 | 36 | const signature = await hs256.sign(secretKey, data); 37 | 38 | const validSignature = await hs256.verify(secretKey, signature, data); 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/HMAC/sign.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "HMAC.sign()" 3 | --- 4 | 5 | # `HMAC.sign()` 6 | 7 | Signs data with a private key and returns the signature. See [`HMAC`](/reference/crypto/HMAC) for an example. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function sign( 13 | secretKey: ArrayBuffer | TypedArray, 14 | data: ArrayBuffer | TypedArray 15 | ): Promise; 16 | ``` 17 | 18 | ### Parameters 19 | 20 | - `secretKey`: Secret key in raw format 21 | - `data`: Data to sign 22 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/HMAC/verify.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "HMAC.verify()" 3 | --- 4 | 5 | # `HMAC.verify()` 6 | 7 | Verifies a signature with a public key and returns `true` if the signature is valid. See [`HMAC`](/reference/crypto/HMAC) for an example. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function sign( 13 | secretKey: ArrayBuffer | TypedArray, 14 | signature: ArrayBuffer | TypedArray, 15 | data: ArrayBuffer | TypedArray 16 | ): Promise; 17 | ``` 18 | 19 | ### Parameters 20 | 21 | - `secretKey`: Secret key in raw format 22 | - `signature` 23 | - `data`: The original signed data 24 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/KeyPair.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "KeyPair" 3 | --- 4 | 5 | # `KeyPair` 6 | 7 | Represents a public/private key pair. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | interface KeyPair { 13 | publicKey: ArrayBuffer; 14 | privateKey: ArrayBuffer; 15 | } 16 | ``` 17 | 18 | ### Properties 19 | 20 | - `publicKey` 21 | - `privateKey` 22 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/RSASSAPKCS1v1_5/generateKeyPair.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "RSASSAPKCS1v1_5.generateKeyPair()" 3 | --- 4 | 5 | # `RSASSAPKCS1v1_5.generateKeyPair()` 6 | 7 | Generates a new public/private key pair. The public key is in SPKI format and the private key is in PKCS#8 format. See [`ECDSA`](/reference/crypto/ECDSA) for an example. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ KeyPair=/reference/crypto/KeyPair 13 | function generateKeyPair(modulusLength?: 2048 | 4096): Promise<$$KeyPair>; 14 | ``` 15 | 16 | ### Parameters 17 | 18 | - `modulusLength` (default: `2048`) 19 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/RSASSAPKCS1v1_5/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "RSASSAPKCS1v1_5" 3 | --- 4 | 5 | # `RSASSAPKCS1v1_5` 6 | 7 | Helper for RSASSA-PKCS1-v1_5. 8 | 9 | ## Constructor 10 | 11 | ```ts 12 | //$ SHAHash=/reference/crypto/SHAHash 13 | function constructor(hash: $$SHAHash): this; 14 | ``` 15 | 16 | ### Parameters 17 | 18 | - `hash` 19 | 20 | ## Method 21 | 22 | - [`generateKeyPair()`](/reference/crypto/RSASSAPKCS1v1_5/generateKeyPair) 23 | - [`sign()`](/reference/crypto/RSASSAPKCS1v1_5/sign) 24 | - [`verify()`](/reference/crypto/RSASSAPKCS1v1_5/verify) 25 | 26 | ## Example 27 | 28 | ```ts 29 | import { RSASSAPKCS1v1_5 } from "oslo/crypto"; 30 | 31 | const rs256 = new RSASSAPKCS1v1_5("SHA-256"); 32 | 33 | const { publicKey, privateKey } = await rs256.generateKeyPair(); 34 | const data = new TextEncoder().encode("hello, world"); 35 | 36 | const signature = await rs256.sign(privateKey, data); 37 | 38 | const validSignature = await rs256.verify(publicKey, signature, data); 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/RSASSAPKCS1v1_5/sign.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "RSASSAPKCS1v1_5.sign()" 3 | --- 4 | 5 | # `RSASSAPKCS1v1_5.sign()` 6 | 7 | Signs data with a private key and returns the signature. See [`RSASSAPKCS1v1_5`](/reference/crypto/RSASSAPKCS1v1_5) for an example. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function sign( 13 | privateKey: ArrayBuffer | TypedArray, 14 | data: ArrayBuffer | TypedArray 15 | ): Promise; 16 | ``` 17 | 18 | ### Parameters 19 | 20 | - `privateKey`: Private key in PKCS#8 format 21 | - `data`: Data to sign 22 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/RSASSAPKCS1v1_5/verify.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "RSASSAPKCS1v1_5.verify()" 3 | --- 4 | 5 | # `RSASSAPKCS1v1_5.verify()` 6 | 7 | Verifies a signature with a public key and returns `true` if the signature is valid. See [`RSASSAPKCS1v1_5`](/reference/crypto/RSASSAPKCS1v1_5) for an example. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function sign( 13 | publicKey: ArrayBuffer | TypedArray, 14 | signature: ArrayBuffer | TypedArray, 15 | data: ArrayBuffer | TypedArray 16 | ): Promise; 17 | ``` 18 | 19 | ### Parameters 20 | 21 | - `publicKey`: Public key in SPKI format 22 | - `signature` 23 | - `data`: The original signed data 24 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/RSASSAPSS/generateKeyPair.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "RSASSAPSS.generateKeyPair()" 3 | --- 4 | 5 | # `RSASSAPSS.generateKeyPair()` 6 | 7 | Generates a new public/private key pair. The public key is in SPKI format and the private key is in PKCS#8 format. See [`RSASSAPSS`](/reference/crypto/RSASSAPSS) for an example. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ KeyPair=/reference/crypto/KeyPair 13 | function generateKeyPair(): Promise<$$KeyPair>; 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/RSASSAPSS/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "RSASSAPSS" 3 | --- 4 | 5 | # `RSASSAPSS` 6 | 7 | Helper for RSASSA-PSS. 8 | 9 | ## Constructor 10 | 11 | ```ts 12 | //$ SHAHash=/reference/crypto/SHAHash 13 | function constructor(hash: $$SHAHash): this; 14 | ``` 15 | 16 | ### Parameters 17 | 18 | - `hash` 19 | 20 | ## Method 21 | 22 | - [`generateKeyPair()`](/reference/crypto/RSASSAPSS/generateKeyPair) 23 | - [`sign()`](/reference/crypto/RSASSAPSS/sign) 24 | - [`verify()`](/reference/crypto/RSASSAPSS/verify) 25 | 26 | ## Example 27 | 28 | ```ts 29 | import { RSASSAPSS } from "oslo/crypto"; 30 | 31 | const ps256 = new RSASSAPSS("SHA-256"); 32 | 33 | const { publicKey, privateKey } = await ps256.generateKeyPair(); 34 | const data = new TextEncoder().encode("hello, world"); 35 | 36 | const signature = await ps256.sign(privateKey, data); 37 | 38 | const validSignature = await ps256.verify(publicKey, signature, data); 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/RSASSAPSS/sign.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "RSASSAPSS.sign()" 3 | --- 4 | 5 | # `RSASSAPSS.sign()` 6 | 7 | Signs data with a private key and returns the signature. See [`RSASSAPSS`](/reference/crypto/RSASSAPSS) for an example. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function sign( 13 | privateKey: ArrayBuffer | TypedArray, 14 | data: ArrayBuffer | TypedArray 15 | ): Promise; 16 | ``` 17 | 18 | ### Parameters 19 | 20 | - `privateKey`: Private key in PKCS#8 format 21 | - `data`: Data to sign 22 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/RSASSAPSS/verify.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "RSASSAPSS.verify()" 3 | --- 4 | 5 | # `RSASSAPSS.verify()` 6 | 7 | Verifies a signature with a public key and returns `true` if the signature is valid. See [`RSASSAPSS`](/reference/crypto/RSASSAPSS) for an example. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function sign( 13 | publicKey: ArrayBuffer | TypedArray, 14 | signature: ArrayBuffer | TypedArray, 15 | data: ArrayBuffer | TypedArray 16 | ): Promise; 17 | ``` 18 | 19 | ### Parameters 20 | 21 | - `publicKey`: Public key in SPKI format 22 | - `signature` 23 | - `data`: The original signed data 24 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/SHAHash.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "SHAHash" 3 | --- 4 | 5 | # `SHAHash` 6 | 7 | ## Definition 8 | 9 | ```ts 10 | type SHAHash = "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512"; 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/alphabet.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "alphabet()" 3 | --- 4 | 5 | # `alphabet()` 6 | 7 | Generates a string with all the characters defined in the provided pattern: 8 | 9 | - `a-z`: `abcdefghijklmnopqrstuvwxyz` 10 | - `A-Z`: `ABCDEFGHIJKLMNOPQRSTUVWXYZ` 11 | - `0-9`: `0123456789` 12 | - `-`: Character `-` 13 | - `_`: Character `_` 14 | 15 | Mostly used for [`generateRandomString()`](/reference/crypto/generateRandomString). Ignores duplicate patterns. 16 | 17 | ## Definition 18 | 19 | ```ts 20 | function alphabet(...patterns: "a-z" | "A-Z" | "0-9" | "-" | "_"): string; 21 | ``` 22 | 23 | ### Parameters 24 | 25 | - `patterns` 26 | 27 | ## Example 28 | 29 | ```ts 30 | import { alphabet } from "oslo/crypto"; 31 | 32 | // "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 33 | alphabet("a-z", "A-Z", "0-9"); 34 | 35 | // "0123456789" 36 | alphabet("0-9", "0-9"); 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/constantTimeEqual.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "constantTimeEqual()" 3 | --- 4 | 5 | # `constantTimeEqual()` 6 | 7 | Constant time comparison.s 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function constantTimeEqual(a: ArrayBuffer | TypedArray, b: ArrayBuffer | TypedArray): boolean; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `a` 18 | - `b` 19 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/generateRandomInteger.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "generateRandomInteger()" 3 | --- 4 | 5 | # `generateRandomInteger()` 6 | 7 | Generates a random integer between 0 (inclusive) and a positive integer (exclusive). Uses cryptographically strong random values. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function generateRandomInteger(max: number): number; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `max` 18 | 19 | ## Example 20 | 21 | ```ts 22 | import { generateRandomInteger } from "oslo/crypto"; 23 | 24 | // random number from 0 to 9 25 | generateRandomInteger(10); 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/generateRandomString.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "generateRandomString()" 3 | --- 4 | 5 | # `generateRandomString()` 6 | 7 | Generates a random string of given length using the provided characters (`alphabet`). See [`alphabet()`](/reference/crypto/alphabet) for creating the alphabet string. Uses cryptographically strong random values. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function generateRandomString(length: number, alphabet: string): string; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `length` 18 | - `alphabet`: A string with all possible characters 19 | 20 | ## Example 21 | 22 | ```ts 23 | import { generateRandomString, alphabet } from "oslo/crypto"; 24 | 25 | // 10-characters long string consisting of the lowercase alphabet and numbers 26 | generateRandomString(10, alphabet("a-z", "0-9")); 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "oslo/crypto" 3 | --- 4 | 5 | # `oslo/crypto` 6 | 7 | Provides light-wrappers around the Web Crypto API for hashing, encryption, creating signatures, and generating cryptographically strong random values 8 | 9 | ## Functions 10 | 11 | - [`alphabet()`](/reference/crypto/alphabet) 12 | - [`constantTimeEqual()`](/reference/crypto/constantTimeEqual) 13 | - [`generateRandomInteger()`](/reference/crypto/generateRandomInteger) 14 | - [`generateRandomString()`](/reference/crypto/generateRandomString) 15 | - [`random()`](/reference/crypto/random) 16 | - [`sha1()`](/reference/crypto/sha1) 17 | - [`sha256()`](/reference/crypto/sha256) 18 | - [`sha384()`](/reference/crypto/sha384) 19 | - [`sha512()`](/reference/crypto/sha512) 20 | 21 | ## Classes 22 | 23 | - [`ECDSA`](/reference/crypto/ECDSA) 24 | - [`HMAC`](/reference/crypto/HMAC) 25 | - [`RSASSAPKCS1v1_5`](/reference/crypto/RSASSAPKCS1v1_5) 26 | - [`RSASSAPSS`](/reference/crypto/RSASSAPSS) 27 | 28 | ## Interfaces 29 | 30 | - [`KeyPair`](/reference/crypto/KeyPair) 31 | 32 | ## Types 33 | 34 | - [`SHAHash`](/reference/crypto/SHAHash) 35 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/random.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "random()" 3 | --- 4 | 5 | # `random()` 6 | 7 | Uses cryptographically strong alternative to `Math.random()`. Returns a floating-point number between 0 (inclusive) and 1 (exclusive) with 52 bits of random. **For generating a random integer, use [`generateRandomInteger()`](/reference/crypto/generateRandomInteger).** 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function random(): number; 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/sha1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "sha1()" 3 | --- 4 | 5 | # `sha1()` 6 | 7 | Generates a SHA-1 hash. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function sha1(data: ArrayBuffer | TypedArray): Promise; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `data` 18 | 19 | ## Example 20 | 21 | ```ts 22 | import { sha1 } from "oslo/crypto"; 23 | import { encodeHex } from "oslo/encoding"; 24 | 25 | const data = new TextEncoder().encode("hello, world"); 26 | const hash = await sha1(data); 27 | const hexHash = encodeHex(hash); 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/sha256.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "sha256()" 3 | --- 4 | 5 | # `sha256()` 6 | 7 | Generates a SHA-256 hash. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function sha256(data: ArrayBuffer | TypedArray): Promise; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `data` 18 | 19 | ## Example 20 | 21 | ```ts 22 | import { sha256 } from "oslo/crypto"; 23 | import { encodeHex } from "oslo/encoding"; 24 | 25 | const data = new TextEncoder().encode("hello, world"); 26 | const hash = await sha256(data); 27 | const hexHash = encodeHex(hash); 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/sha384.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "sha384()" 3 | --- 4 | 5 | # `sha384()` 6 | 7 | Generates a SHA-384 hash. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function sha384(data: ArrayBuffer | TypedArray): Promise; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `data` 18 | 19 | ## Example 20 | 21 | ```ts 22 | import { sha384 } from "oslo/crypto"; 23 | import { encodeHex } from "oslo/encoding"; 24 | 25 | const data = new TextEncoder().encode("hello, world"); 26 | const hash = await sha384(data); 27 | const hexHash = encodeHex(hash); 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/pages/reference/crypto/sha512.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "sha512" 3 | --- 4 | 5 | # `sha512()` 6 | 7 | Generates a SHA-512 hash. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function sha512(data: ArrayBuffer | TypedArray): Promise; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `data` 18 | 19 | ## Example 20 | 21 | ```ts 22 | import { sha512 } from "oslo/crypto"; 23 | import { encodeHex } from "oslo/encoding"; 24 | 25 | const data = new TextEncoder().encode("hello, world"); 26 | const hash = await sha512(data); 27 | const hexHash = encodeHex(hash); 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/Base32Encoding/decode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Base32Encoding.decode()" 3 | --- 4 | 5 | # `Base32Encoding.decode()` 6 | 7 | Decodes a base 32 encoded string. By default, strict mode is enabled and the encoded string must include padding. Throws if the encoded string is malformed. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function decode( 13 | data: string, 14 | options?: { 15 | strict?: boolean; 16 | } 17 | ): Uint8Array; 18 | ``` 19 | 20 | ### Parameters 21 | 22 | - `data` 23 | - `options` 24 | - `strict` (default: `true`): Requires the input to have padding 25 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/Base32Encoding/encode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Base32Encoding.encode()" 3 | --- 4 | 5 | # `Base32Encoding.encode()` 6 | 7 | Encodes data with base 32. Includes padding by default. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function encode( 13 | data: Uint8Array, 14 | options?: { 15 | includePadding?: boolean; 16 | } 17 | ): string; 18 | ``` 19 | 20 | ### Parameters 21 | 22 | - `data` 23 | - `options` 24 | - `includePadding` (default: `true`) 25 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/Base32Encoding/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Base32Encoding" 3 | --- 4 | 5 | # `Base32Encoding` 6 | 7 | Radix 32 encoding/decoding scheme. Characters are case sensitive. Use [`base32`](/reference/encoding/base32) or [`base32hex`](/reference/encoding/base32hex) for Base 32 encoding based on [RFC 4648](https://rfc-editor.org/rfc/rfc4648.html). 8 | 9 | ## Constructor 10 | 11 | ```ts 12 | function constructor(alphabet: string): this; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `alphabet`: A string of 32 characters 18 | 19 | ## Methods 20 | 21 | - [`decode()`](/reference/encoding/Base32Encoding/decode) 22 | - [`encode()`](/reference/encoding/Base32Encoding/encode) 23 | 24 | ## Example 25 | 26 | ```ts 27 | import { Base32Encoding } from "oslo/encoding"; 28 | 29 | const base32 = new Base32Encoding("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"); 30 | const encoded = base32.encode(new Uint8Array(8)); 31 | const decoded = base32.encode(encoded); 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/Base64Encoding/decode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Base64Encoding.decode()" 3 | --- 4 | 5 | # `Base64Encoding.decode()` 6 | 7 | Decodes a base 64 encoded string. By default, strict mode is enabled and the encoded string must include padding. Throws if the encoded string is malformed. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function decode( 13 | data: string, 14 | options?: { 15 | strict?: boolean; 16 | } 17 | ): Uint8Array; 18 | ``` 19 | 20 | ### Parameters 21 | 22 | - `data` 23 | - `options` 24 | - `strict` (default: `true`): Requires the input to have padding 25 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/Base64Encoding/encode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Base64Encoding.encode()" 3 | --- 4 | 5 | # `Base64Encoding.encode()` 6 | 7 | Encodes data. Includes padding by default. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function encode( 13 | data: Uint8Array, 14 | options?: { 15 | includePadding?: boolean; 16 | } 17 | ): string; 18 | ``` 19 | 20 | ### Parameters 21 | 22 | - `data` 23 | - `options` 24 | - `includePadding` (default: `true`) 25 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/Base64Encoding/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Base64Encoding" 3 | --- 4 | 5 | # `Base64Encoding` 6 | 7 | Radix 64 encoding/decoding scheme. Characters are case sensitive. Use [`base64`](/reference/encoding/base64) or [`base64url`](/reference/encoding/base64url) for Base 64 encoding based on [RFC 4648](https://rfc-editor.org/rfc/rfc4648.html). 8 | 9 | ## Constructor 10 | 11 | ```ts 12 | function constructor(alphabet: string): this; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `alphabet`: A string of 64 characters 18 | 19 | ## Methods 20 | 21 | - [`decode()`](/reference/encoding/Base64Encoding/decode) 22 | - [`encode()`](/reference/encoding/Base64Encoding/encode) 23 | 24 | ## Example 25 | 26 | ```ts 27 | import { Base64Encoding } from "oslo/encoding"; 28 | 29 | const base64 = new Base64Encoding( 30 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 31 | ); 32 | const encoded = base64.encode(new Uint8Array(8)); 33 | const decoded = base64.encode(encoded); 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/base32.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "base32" 3 | --- 4 | 5 | # `base32` 6 | 7 | Implements Base 32 based on [RFC 4648 §6](https://datatracker.ietf.org/doc/html/rfc4648#section-6) with [`Base32Encoding`](/reference/encoding/Base32Encoding). 8 | 9 | ```ts 10 | import { base32 } from "oslo/encoding"; 11 | 12 | const encoded = base32.encode(message); 13 | const decoded = base32.decode(encoded); 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/base32hex.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "base32hex" 3 | --- 4 | 5 | # `base32hex` 6 | 7 | Implements Base 32 with extended hex alphabet based on [RFC 4648 §7](https://datatracker.ietf.org/doc/html/rfc4648#section-7) with [`Base32Encoding`](/reference/encoding/Base32Encoding). 8 | 9 | ```ts 10 | import { base32hex } from "oslo/encoding"; 11 | 12 | const encoded = base32hex.encode(message); 13 | const decoded = base32hex.decode(encoded); 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/base64.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "base64" 3 | --- 4 | 5 | # `base64` 6 | 7 | Implements Base 64 encoding based on [RFC 4648 §4](https://datatracker.ietf.org/doc/html/rfc4648#section-4) with [`Base64Encoding`](/reference/encoding/Base64Encoding). 8 | 9 | ```ts 10 | import { base64 } from "oslo/encoding"; 11 | 12 | const encoded = base64.encode(message); 13 | const decoded = base64.decode(encoded); 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/base64url.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "base64url" 3 | --- 4 | 5 | # `base64url` 6 | 7 | Implements Base 64 URL encoding based on [RFC 4648 §5](https://datatracker.ietf.org/doc/html/rfc4648#section-5) with [`Base64Encoding`](/reference/encoding/Base64Encoding). Includes padding by default. 8 | 9 | ```ts 10 | import { base64url } from "oslo/encoding"; 11 | 12 | const encoded = base64url.encode(message); 13 | const decoded = base64url.decode(encoded); 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/decodeBase32.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "decodeBase32()" 3 | --- 4 | 5 | # `decodeBase32()` 6 | 7 | **Deprecated - Use [`base32`](/reference/encoding/base32) instead.** 8 | 9 | Decodes base32 strings. This does not check the length and ignores padding. Use [`encodeBase32()`](/reference/encoding/encodeBase32) to encode into base32 strings. 10 | 11 | ## Definition 12 | 13 | ```ts 14 | function decodeBase32(encoded: string): Uint8Array; 15 | ``` 16 | 17 | ### Parameters 18 | 19 | - `encoded` 20 | 21 | ## Example 22 | 23 | ```ts 24 | import { decodeBase32 } from "oslo/encoding"; 25 | 26 | const data = decodeBase32(encoded); 27 | const text = new TextDecoder().decode(data); 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/decodeBase64.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "decodeBase64()" 3 | --- 4 | 5 | # `decodeBase64()` 6 | 7 | **Deprecated - Use [`base64`](/reference/encoding/base64) instead.** 8 | 9 | Decodes base64 strings. This does not check the length and ignores padding. Use [`encodeBase64()`](/reference/encoding/encodeBase64) to encode into base64 strings. 10 | 11 | ## Definition 12 | 13 | ```ts 14 | function decodeBase64(encoded: string): Uint8Array; 15 | ``` 16 | 17 | ### Parameters 18 | 19 | - `encoded` 20 | 21 | ## Example 22 | 23 | ```ts 24 | import { decodeBase64 } from "oslo/encoding"; 25 | 26 | const data = decodeBase64(encoded); 27 | const text = new TextDecoder().decode(data); 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/decodeBase64url.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "decodeBase64url()" 3 | --- 4 | 5 | # `decodeBase64url()` 6 | 7 | **Deprecated - Use [`base64url`](/reference/encoding/base64url) instead.** 8 | 9 | Decodes base64 URL strings. Use [`encodeBase64url()`](/reference/encoding/encodeBase64url) to encode into base64 URL strings. 10 | 11 | ## Definition 12 | 13 | ```ts 14 | function decodeBase64url(encoded: string): Uint8Array; 15 | ``` 16 | 17 | ### Parameters 18 | 19 | - `encoded` 20 | 21 | ## Example 22 | 23 | ```ts 24 | import { decodeBase64url } from "oslo/encoding"; 25 | 26 | const data = decodeBase64url(encoded); 27 | const text = new TextDecoder().decode(data); 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/decodeHex.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "decodeHex()" 3 | --- 4 | 5 | # `decodeHex()` 6 | 7 | Decodes hex-encoded strings based on [RFC 4648 §8](https://datatracker.ietf.org/doc/html/rfc4648#section-8). Supports both lowercase and uppercase hex strings anf throws if the hex string is malformed. Use [`encodeHex()`](/reference/encoding/encodeHex) to encode into hex strings. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function decodeHex(data: string): Uint8Array; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `data` 18 | 19 | ## Example 20 | 21 | ```ts 22 | import { decodeHex } from "oslo/encoding"; 23 | 24 | const data = decodeHex(encoded); 25 | const text = new TextDecoder().decode(data); 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/encodeBase32.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "encodeBase32()" 3 | --- 4 | 5 | # `encodeBase32()` 6 | 7 | **Deprecated - Use [`base32`](/reference/encoding/base32) instead.** 8 | 9 | Encodes data into base32 string. Use [`decodeBase32()`](/reference/encoding/decodeBase32) to decode base32 strings. 10 | 11 | ## Definition 12 | 13 | ```ts 14 | function encodeBase32( 15 | data: ArrayBuffer | TypedArray, 16 | options?: { 17 | padding?: boolean; 18 | } 19 | ): string; 20 | ``` 21 | 22 | ### Parameters 23 | 24 | - `data` 25 | - `options.padding` (default: `true`): Set to `false` to remove padding 26 | 27 | ## Example 28 | 29 | ```ts 30 | import { encodeBase32 } from "oslo/encoding"; 31 | 32 | const data = new TextEncoder("hello, world"); 33 | const encoded = encodeBase32(data); 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/encodeBase64.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "encodeBase64()" 3 | --- 4 | 5 | # `encodeBase64()` 6 | 7 | **Deprecated - Use [`base64`](/reference/encoding/base64) instead.** 8 | 9 | Encodes data into base64 string. Use [`decodeBase64()`](/reference/encoding/decodeBase64) to decode base64 strings. 10 | 11 | ## Definition 12 | 13 | ```ts 14 | function encodeBase64( 15 | data: ArrayBuffer | TypedArray, 16 | options?: { 17 | padding?: boolean; 18 | } 19 | ): string; 20 | ``` 21 | 22 | ### Parameters 23 | 24 | - `data` 25 | - `options.padding` (default: `true`): Set to `false` to remove padding 26 | 27 | ## Example 28 | 29 | ```ts 30 | import { encodeBase64 } from "oslo/encoding"; 31 | 32 | const data = new TextEncoder("hello, world"); 33 | const encoded = encodeBase64(data); 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/encodeBase64url.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "encodeBase64url()" 3 | --- 4 | 5 | # `encodeBase64url()` 6 | 7 | **Deprecated - Use [`base64url`](/reference/encoding/base64url) instead.** 8 | 9 | Encodes data into base64 URL string. Use [`decodeBase64url()`](/reference/encoding/decodeBase64url) to decode base64 URL strings. 10 | 11 | ## Definition 12 | 13 | ```ts 14 | function encodeBase64url(data: ArrayBuffer | TypedArray): string; 15 | ``` 16 | 17 | ### Parameters 18 | 19 | - `data` 20 | 21 | ## Example 22 | 23 | ```ts 24 | import { encodeBase64url } from "oslo/encoding"; 25 | 26 | const data = new TextEncoder("hello, world"); 27 | const encoded = encodeBase64url(data); 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/encodeHex.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "encodeHex()" 3 | --- 4 | 5 | # `encodeHex()` 6 | 7 | Encodes data into lowercase hex based on [RFC 4648 §8](https://datatracker.ietf.org/doc/html/rfc4648#section-8). Use [`decodeHex()`](/reference/encoding/decodeHex) to decode hex strings. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function encodeHex(data: ArrayBuffer | TypedArray): string; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `data` 18 | 19 | ## Example 20 | 21 | ```ts 22 | import { encodeHex } from "oslo/encoding"; 23 | 24 | const data = new TextEncoder("hello, world"); 25 | const encoded = encodeHex(data); 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/pages/reference/encoding/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "oslo/encoding" 3 | --- 4 | 5 | # `oslo/encoding` 6 | 7 | Provides utilities for encoding and decoding with various formats. 8 | 9 | - [`base32`](/reference/encoding/base32) 10 | - [`base32hex`](/reference/encoding/base32hex) 11 | - [`Base32Encoding`](/reference/encoding/Base32Encoding) 12 | - [`base64`](/reference/encoding/base64) 13 | - [`base64url`](/reference/encoding/base64url) 14 | - [`Base64Encoding`](/reference/encoding/Base64Encoding) 15 | - [`decodeHex()`](/reference/encoding/decodeHex) 16 | - [`encodeHex()`](/reference/encoding/encodeHex) 17 | - [`encodeBase32()`](/reference/encoding/encodeBase32) (_deprecated_) 18 | - [`encodeBase64()`](/reference/encoding/encodeBase64) (_deprecated_) 19 | - [`encodeBase64url()`](/reference/encoding/encodeBase64url) (_deprecated_) 20 | - [`decodeBase32()`](/reference/encoding/decodeBase32) (_deprecated_) 21 | - [`decodeBase64()`](/reference/encoding/decodeBase64) (_deprecated_) 22 | - [`decodeBase64url()`](/reference/encoding/decodeBase64url) (_deprecated_) 23 | -------------------------------------------------------------------------------- /docs/pages/reference/jwt/JWT.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "JWT" 3 | --- 4 | 5 | # `JWT` 6 | 7 | Represents a JWT. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ JWTAlgorithm=/reference/jwt/JWTAlgorithm 13 | interface JWT { 14 | value: string; 15 | headers: object; 16 | payload: object; 17 | algorithm: $$JWTAlgorithm; 18 | expiresAt: Date | null; 19 | issuer: string | null; 20 | subject: string | null; 21 | audiences: string[] | null; 22 | notBefore: Date | null; 23 | issuedAt: Date | null; 24 | jwtId: string | null; 25 | parts: [header: string, payload: string, signature: string]; 26 | } 27 | ``` 28 | 29 | ### Properties 30 | 31 | - `value`: JWT string 32 | - `headers` 33 | - `payload` 34 | - `algorithm` 35 | - `expiresAt`: `exp` claim 36 | - `issuer`: `iss` claim 37 | - `subject`: `sub` claim 38 | - `audiences`: `aud` claims 39 | - `notBefore`: `nbf` claim 40 | - `issuedAt`: `iat` claim 41 | - `jwtId`: `jti` claim 42 | - `parts`: JWT string separated into 3 parts 43 | -------------------------------------------------------------------------------- /docs/pages/reference/jwt/JWTAlgorithm.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "JWTAlgorithm" 3 | --- 4 | 5 | # `JWTAlgorithm` 6 | 7 | Represents the algorithm supported by Oslo for signing JWTs. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | type JWTAlgorithm = 13 | | "HS256" 14 | | "HS384" 15 | | "HS512" 16 | | "RS256" 17 | | "RS384" 18 | | "RS512" 19 | | "ES256" 20 | | "ES384" 21 | | "ES512" 22 | | "PS256" 23 | | "PS384" 24 | | "PS512"; 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/pages/reference/jwt/createJWT.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "createJWT()" 3 | --- 4 | 5 | # `createJWT()` 6 | 7 | Creates a new JWT. Claims are not included by default and must by defined with `options`. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ JWTAlgorithm=/reference/jwt/JWTAlgorithm 13 | //$ TimeSpan=/reference/main/TimeSpan 14 | function createJWT( 15 | algorithm: $$JWTAlgorithm, 16 | key: ArrayBuffer | TypedArray, 17 | payloadClaims: Record, 18 | options?: { 19 | headers?: Record; 20 | expiresIn?: $$TimeSpan; 21 | issuer?: string; 22 | subject?: string; 23 | audiences?: string[]; 24 | notBefore?: Date; 25 | includeIssuedTimestamp?: boolean; 26 | jwtId?: string; 27 | } 28 | ): Promise; 29 | ``` 30 | 31 | ### Parameters 32 | 33 | - `algorithm` 34 | - `key`: Secret key for HMAC, and private key for ECDSA and RSA 35 | - `payloadClaims` 36 | - `options`: 37 | - `headers`: Custom headers 38 | - `expiresIn`: How long the JWT is valid for (for `exp` claim) 39 | - `issuer`: `iss` claim 40 | - `subject`: `sub` claim 41 | - `audiences`: `aud` claims 42 | - `notBefore`: `nbf` claim 43 | - `includeIssuedTimestamp` (default: `false`): Set to `true` to include `iat` claim 44 | - `jwtId`: `jti` claim 45 | 46 | ## Example 47 | 48 | ```ts 49 | import { HMAC } from "oslo/crypto"; 50 | import { createJWT, validateJWT, parseJWT } from "oslo/jwt"; 51 | import { TimeSpan } from "oslo"; 52 | 53 | const secret = await new HMAC("SHA-256").generateKey(); 54 | 55 | const payload = { 56 | message: "hello, world" 57 | }; 58 | 59 | const jwt = await createJWT("HS256", secret, payload, { 60 | headers: { 61 | kid 62 | }, 63 | expiresIn: new TimeSpan(30, "d"), 64 | issuer, 65 | subject, 66 | audiences, 67 | includeIssuedTimestamp: true 68 | }); 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/pages/reference/jwt/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "oslo/jwt" 3 | --- 4 | 5 | # `oslo/jwt` 6 | 7 | Provides utilities for working with JWTs. Supports the following algorithms: 8 | 9 | - HMAC: `HS256`, `HS384`, `HS512` 10 | - ECDSA: `ES256`, `ES384`, `ES512` 11 | - RSASSA-PKCS1-v1_5: `RS256`, `RS384`, `RS512` 12 | - RSASSA-PSS: `PS256`, `PS384`, `PS512` 13 | 14 | ## Functions 15 | 16 | - [`createJWT()`](/reference/jwt/createJWT) 17 | - [`parseJWT()`](/reference/jwt/parseJWT) 18 | - [`validateJWT()`](/reference/jwt/validateJWT) 19 | 20 | ## Interfaces 21 | 22 | - [`JWT`](/reference/jwt/JWT) 23 | 24 | ## Types 25 | 26 | - [`JWTAlgorithm`](/reference/jwt/JWTAlgorithm) 27 | -------------------------------------------------------------------------------- /docs/pages/reference/jwt/parseJWT.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "parseJWT()" 3 | --- 4 | 5 | # `parseJWT()` 6 | 7 | Parses a JWT string. **This does NOT validate the JWT signature and claims** including expiration. Use [`validateJWT()`](/reference/jwt/validateJWT) instead. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ JWT=/reference/jwt/JWT 13 | function parseJWT(jwt: string): $$JWT | null; 14 | ``` 15 | 16 | ### Parameters 17 | 18 | - `jwt`: JWT string 19 | 20 | ## Example 21 | 22 | ```ts 23 | import { parseJWT } from "oslo/jwt"; 24 | 25 | const jwt = parseJWT( 26 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8ifQ.yP03DaEblJkk9mR-Y5L7YCMzJgHL-RDPx90aXz-cuAI" 27 | ); 28 | const message = jwt.payload.message; 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/pages/reference/jwt/validateJWT.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "validateJWT()" 3 | --- 4 | 5 | # `validateJWT()` 6 | 7 | Parses a JWT string and validates it, including the signature, expiration, and not-before date. Throws if the JWT is invalid or expired. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ JWTAlgorithm=/reference/jwt/JWTAlgorithm 13 | //$ TimeSpan=/reference/main/TimeSpan 14 | //$ JWT=/reference/jwt/JWT 15 | function validateJWT( 16 | algorithm: JWTAlgorithm, 17 | key: ArrayBuffer | TypedArray, 18 | jwt: string 19 | ): Promise<$$JWT>; 20 | ``` 21 | 22 | ### Parameters 23 | 24 | - `algorithm` 25 | - `key`: Secret key for HMAC, and private key for ECDSA and RSA 26 | - `jwt`: JWT string 27 | 28 | ## Example 29 | 30 | ```ts 31 | import { validateJWT } from "oslo/jwt"; 32 | 33 | try { 34 | const jwt = validateJWT( 35 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8ifQ.yP03DaEblJkk9mR-Y5L7YCMzJgHL-RDPx90aXz-cuAI" 36 | ); 37 | const message = jwt.payload.message; 38 | } catch { 39 | // invalid signature 40 | // expired token 41 | // inactive token (`nbf`) 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/pages/reference/main/TimeSpan/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "TimeSpan" 3 | --- 4 | 5 | # `TimeSpan` 6 | 7 | Represents a time-span. Supports negative values. 8 | 9 | ## Constructor 10 | 11 | ```ts 12 | //$ TimeSpanUnit=/reference/main/TimeSpanUnit 13 | function constructor(value: number, unit: $$TimeSpanUnit): this; 14 | ``` 15 | 16 | ### Parameters 17 | 18 | - `value` 19 | - `unit`: `ms` for milliseconds, `s` for seconds, etc 20 | 21 | ## Methods 22 | 23 | - [`milliseconds()`](/reference/main/TimeSpan/milliseconds) 24 | - [`seconds()`](/reference/main/TimeSpan/seconds) 25 | 26 | ## Properties 27 | 28 | ```ts 29 | //$ TimeSpanUnit=/reference/main/TimeSpanUnit 30 | interface Properties { 31 | unit: $$TimeSpanUnit; 32 | value: number; 33 | } 34 | ``` 35 | 36 | - `unit` 37 | - `value` 38 | 39 | ## Example 40 | 41 | ```ts 42 | import { TimeSpan } from "oslo"; 43 | 44 | const halfSeconds = new TimeSpan(500, "ms"); 45 | const tenSeconds = new TimeSpan(10, "s"); 46 | const halfHour = new TimeSpan(30, "m"); 47 | const oneHour = new TimeSpan(1, "h"); 48 | const oneDay = new TimeSpan(1, "d"); 49 | const oneWeek = new TimeSpan(1, "w"); 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/pages/reference/main/TimeSpan/milliseconds.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Timespan.milliseconds()" 3 | --- 4 | 5 | # `Timespan.milliseconds()` 6 | 7 | Returns the time-span in milliseconds. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function milliseconds(): number; 13 | ``` 14 | 15 | ## Example 16 | 17 | ```ts 18 | // 60 * 1000 = 60,000 ms 19 | new TimeSpan("60", "s").milliseconds(); 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/pages/reference/main/TimeSpan/seconds.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Timespan.seconds()" 3 | --- 4 | 5 | # `Timespan.seconds()` 6 | 7 | Returns the time-span in seconds. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function seconds(): number; 13 | ``` 14 | 15 | ## Example 16 | 17 | ```ts 18 | // 60 * 60 = 3600 s 19 | new TimeSpan(1, "h").seconds(); 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/pages/reference/main/TimeSpanUnit.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "TimeSpanUnit" 3 | --- 4 | 5 | # `TimeSpanUnit` 6 | 7 | ## Definition 8 | 9 | ```ts 10 | type TimeSpanUnit = "ms" | "s" | "m" | "h" | "d" | "w"; 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/pages/reference/main/createDate.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "createDate()" 3 | --- 4 | 5 | # `createDate()` 6 | 7 | Creates a new `Date` by adding the provided time-span to the current time. Mostly for defining expiration times. Supports negative time span. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ TimeSpan=/reference/main/TimeSpan 13 | function createDate(timeSpan: $$TimeSpan): Date; 14 | ``` 15 | 16 | ### Parameters 17 | 18 | - `timeSpan` 19 | 20 | ## Example 21 | 22 | ```ts 23 | import { createDate, TimeSpan } from "oslo"; 24 | 25 | const tomorrow = createDate(new TimeSpan(1, "d")); 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/pages/reference/main/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "oslo" 3 | --- 4 | 5 | # `oslo` 6 | 7 | Provides basic utilities used by other modules. 8 | 9 | ## Functions 10 | 11 | - [`createDate()`](/reference/main/createDate) 12 | - [`isWithinExpirationDate()`](/reference/main/isWithinExpirationDate) 13 | 14 | ## Classes 15 | 16 | - [`TimeSpan`](/reference/main/TimeSpan) 17 | 18 | ## Types 19 | 20 | - [`TimeSpanUnit`](/reference/main/TimeSpanUnit) 21 | -------------------------------------------------------------------------------- /docs/pages/reference/main/isWithinExpirationDate.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "isWithinExpirationDate()" 3 | --- 4 | 5 | # `isWithinExpirationDate()` 6 | 7 | Checks if the current time is before the provided expiration `Date`. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ TimeSpan=/reference/main/TimeSpan 13 | function isWithinExpirationDate(expirationDate: Date): boolean; 14 | ``` 15 | 16 | ### Parameters 17 | 18 | - `expirationDate` 19 | 20 | ## Example 21 | 22 | ```ts 23 | import { createDate, TimeSpan, isWithinExpirationDate } from "oslo"; 24 | 25 | const tomorrow = createDate(new TimeSpan(1, "d")); 26 | const yesterday = createDate(new TimeSpan(-1, "d")); 27 | 28 | isWithinExpirationDate(tomorrow); // true 29 | isWithinExpirationDate(yesterday); // false 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/pages/reference/oauth2/OAuth2Client/createAuthorizationURL.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "OAuth2Client.createAuthorizationURL()" 3 | --- 4 | 5 | # `OAuth2Client.createAuthorizationURL()` 6 | 7 | Creates a new authorization URL. This method supports both `plain` and `S256` PKCE code challenge methods. By default, no scopes are included. 8 | 9 | See [`oslo/oauth2`](/reference/oauth2) for a full example. For generating the state and code verifier, see [`generateState()`](/reference/oauth2/generateState) and [`generateCodeVerifier()`](/reference/oauth2/generateCodeVerifier) respectively. 10 | 11 | ## Definition 12 | 13 | ```ts 14 | function createAuthorizationURL(options?: { 15 | state?: string; 16 | codeChallengeMethod?: "S256" | "plain"; 17 | codeVerifier?: string; 18 | scopes?: string[]; 19 | }): Promise; 20 | ``` 21 | 22 | ### Parameters 23 | 24 | - `options` 25 | - `state` 26 | - `codeVerifier`: Code verifier for PKCE flow 27 | - `codeChallengeMethod` (default: `"S256"`): Ignored if `codeVerifier` is undefined 28 | - `scopes`: An array of scopes 29 | 30 | ## Example 31 | 32 | ```ts 33 | //$ generateState=/reference/oauth2/generateState 34 | //$ generateCodeVerifier=/reference/oauth2/generateCodeVerifier 35 | import { $$generateState, $$generateCodeVerifier } from "oslo/oauth2"; 36 | 37 | const state = generateState(); 38 | const codeVerifier = generateCodeVerifier(); 39 | 40 | const url = await oauth2Client.createAuthorizationURL({ 41 | state, 42 | codeVerifier, 43 | scopes: ["profile", "openid"] 44 | }); 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/pages/reference/oauth2/OAuth2Client/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "OAuth2Client" 3 | --- 4 | 5 | # `OAuth2Client` 6 | 7 | Helper for OAuth 2.0, as defined in [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749). See [`oslo/oauth2`](/reference/oauth2) for a full example. 8 | 9 | ## Constructor 10 | 11 | ```ts 12 | function constructor( 13 | clientId: string, 14 | authorizeEndpoint: string, 15 | tokenEndpoint: string, 16 | options?: { 17 | redirectURI?: string; 18 | } 19 | ): this; 20 | ``` 21 | 22 | ### Parameters 23 | 24 | - `clientId` 25 | - `authorizeEndpoint` 26 | - `tokenEndpoint` 27 | - `options` 28 | - `redirectURI` 29 | 30 | ## Methods 31 | 32 | - [`createAuthorizationURL()`](/reference/oauth2/OAuth2Client/createAuthorizationURL) 33 | - [`refreshAccessToken()`](/reference/oauth2/OAuth2Client/refreshAccessToken) 34 | - [`validateAuthorizationCode()`](/reference/oauth2/OAuth2Client/validateAuthorizationCode) 35 | 36 | ## Properties 37 | 38 | ```ts 39 | interface Properties { 40 | clientId: string; 41 | } 42 | ``` 43 | 44 | - `clientId` 45 | -------------------------------------------------------------------------------- /docs/pages/reference/oauth2/OAuth2Client/refreshAccessToken.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "OAuth2Client.refreshAccessToken()" 3 | --- 4 | 5 | # `OAuth2Client.refreshAccessToken()` 6 | 7 | Refreshes an access tokens with the refresh token. This sends a POST request (`application/x-www-form-urlencoded`) to the token endpoint defined when initializing [`OAuth2Client`](/reference/oauth2/OAuth2Client) and returns the JSON-parsed response body. You can define the request body type with `_TokenResponseBody` type parameter. 8 | 9 | By default, credentials (client secret) is sent via the HTTP basic auth scheme. To send it inside the request body (ie. search params), set `options.authenticateWith` to `"request_body"`. 10 | 11 | This throws a [`OAuth2RequestError`](/reference/oauth2/OAuth2RequestError) on error responses, and `fetch()` error when it fails to connect to the endpoint. 12 | 13 | See [`oslo/oauth2`](/reference/oauth2) for a full example. 14 | 15 | ## Definition 16 | 17 | ```ts 18 | function refreshAccessToken<_TokenResponseBody extends TokenResponseBody>( 19 | refreshToken: string, 20 | options?: { 21 | credentials?: string; 22 | authenticateWith?: "http_basic_auth" | "request_body"; 23 | scopes?: string[]; 24 | } 25 | ): Promise<_TokenResponseBody>; 26 | ``` 27 | 28 | ### Parameters 29 | 30 | - `refreshToken`: Refresh token 31 | - `options` 32 | - `credentials`: Client password or secret for authenticated requests 33 | - `authenticateWith` (default: `"http_basic_auth"`): How the credentials should be sent 34 | - `scopes` 35 | 36 | ### Type parameters 37 | 38 | - `_TokenResponseBody`: JSON-parsed success response body from the token endpoint 39 | 40 | ## Example 41 | 42 | ```ts 43 | //$ OAuth2RequestError=/reference/oauth2/OAuth2RequestError 44 | //$ TokenResponseBody=/reference/oauth2/TokenResponseBody 45 | //$ oauth2Client=/reference/oauth2/OAuth2Client 46 | import { $$OAuth2RequestError } from "oslo/oauth2"; 47 | import type { $$TokenResponseBody } from "oslo/oauth2"; 48 | 49 | interface ResponseBody extends TokenResponseBody { 50 | refresh_token: string; 51 | } 52 | try { 53 | const tokens = await $$oauth2Client.refreshAccessToken(code, { 54 | credentials: clientSecret, 55 | authenticateWith: "request_body" // send client secret inside body 56 | }); 57 | } catch (e) { 58 | if (e instanceof OAuth2RequestError) { 59 | // invalid credentials etc 60 | } 61 | // unknown error 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /docs/pages/reference/oauth2/OAuth2Client/validateAuthorizationCode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "OAuth2Client.validateAuthorizationCode()" 3 | --- 4 | 5 | # `OAuth2Client.validateAuthorizationCode()` 6 | 7 | Validates the authorization code included in the callback. This sends a POST request (`application/x-www-form-urlencoded`) to the token endpoint defined when initializing [`OAuth2Client`](/reference/oauth2/OAuth2Client) and returns the JSON-parsed response body. You can define the request body type with `_TokenResponseBody` type parameter. 8 | 9 | By default, credentials (client secret) is sent via the HTTP basic auth scheme. To send it inside the request body (ie. search params), set `options.authenticateWith` to `"request_body"`. 10 | 11 | This throws a [`OAuth2RequestError`](/reference/oauth2/OAuth2RequestError) on error responses, and `fetch()` error when it fails to connect to the endpoint. 12 | 13 | See [`oslo/oauth2`](/reference/oauth2) for a full example. 14 | 15 | ## Definition 16 | 17 | ```ts 18 | function validateAuthorizationCode<_TokenResponseBody extends TokenResponseBody>( 19 | authorizationCode: string, 20 | options?: { 21 | codeVerifier?: string; 22 | credentials?: string; 23 | authenticateWith?: "http_basic_auth" | "request_body"; 24 | } 25 | ): Promise<_TokenResponseBody>; 26 | ``` 27 | 28 | ### Parameters 29 | 30 | - `authorizationCode`: `code` param in callback request 31 | - `options` 32 | - `codeVerifier`: Stored code verifier for PKCE flow 33 | - `credentials`: Client password or secret for authenticated requests 34 | - `authenticateWith` (default: `"http_basic_auth"`): How the credentials should be sent 35 | 36 | ### Type parameters 37 | 38 | - `_TokenResponseBody`: JSON-parsed success response body from the token endpoint 39 | 40 | ## Example 41 | 42 | ```ts 43 | //$ OAuth2RequestError=/reference/oauth2/OAuth2RequestError 44 | //$ TokenResponseBody=/reference/oauth2/TokenResponseBody 45 | //$ oauth2Client=/reference/oauth2/OAuth2Client 46 | import { $$OAuth2RequestError } from "oslo/oauth2"; 47 | import type { $$TokenResponseBody } from "oslo/oauth2"; 48 | 49 | interface ResponseBody extends TokenResponseBody { 50 | refresh_token: string; 51 | } 52 | try { 53 | const tokens = await $$oauth2Client.validateAuthorizationCode(code, { 54 | codeVerifier: storedCodeVerifier, 55 | credentials: clientSecret, 56 | authenticateWith: "request_body" // send client secret inside body 57 | }); 58 | } catch (e) { 59 | if (e instanceof OAuth2RequestError) { 60 | // invalid credentials etc 61 | } 62 | // unknown error 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/pages/reference/oauth2/OAuth2RequestError.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "OAuth2RequestError" 3 | extends: "Error" 4 | --- 5 | 6 | # `OAuth2RequestError` 7 | 8 | Error thrown by [`OAuth2Client.validateAuthorizationCode()`](/reference/oauth/OAuth2Client2/validateAuthorizationCode) when the token endpoint returns an error response. See [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2) for a full reference on error messages. 9 | 10 | ## Definition 11 | 12 | ```ts 13 | interface Properties extends Error { 14 | request: Request; 15 | description: string | null; 16 | } 17 | ``` 18 | 19 | ### Properties 20 | 21 | - `message`: OAuth 2.0 error message 22 | - `request`: The original request 23 | - `description`: OAuth 2.0 error description 24 | -------------------------------------------------------------------------------- /docs/pages/reference/oauth2/TokenResponseBody.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "TokenResponseBody" 3 | --- 4 | 5 | # `TokenResponseBody` 6 | 7 | Represents the JSON-parsed success response body from the token endpoint. You can extend this interface to include properties such as `refresh_token`. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | interface TokenResponseBody { 13 | access_token: string; 14 | token_type?: string; 15 | expires_in?: number; 16 | refresh_token?: string; 17 | scope?: string; 18 | } 19 | ``` 20 | 21 | ### Properties 22 | 23 | - `access_token`: The access token 24 | -------------------------------------------------------------------------------- /docs/pages/reference/oauth2/generateCodeVerifier.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "generateCodeVerifier()" 3 | --- 4 | 5 | # `generateCodeVerifier()` 6 | 7 | Generates a new code verifier for PKCE flow. See [`oslo/oauth2`](/reference/oauth2) for a full example. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function generateCodeVerifier(): string; 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/pages/reference/oauth2/generateState.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "generateState()" 3 | --- 4 | 5 | # `generateState()` 6 | 7 | Generates a new OAuth state. See [`oslo/oauth2`](/reference/oauth2) for a full example. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function generateState(): string; 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/pages/reference/oauth2/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "oslo/oauth2" 3 | --- 4 | 5 | # `oslo/oauth2` 6 | 7 | Provides utilities for working OAuth 2.0 based on [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) and [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636). Can be used for OpenID Connect. 8 | 9 | ## Functions 10 | 11 | - [`generateCodeVerifier()`](/reference/oauth2/generateCodeVerifier) 12 | - [`generateState()`](/reference/oauth2/generateState) 13 | 14 | ## Classes 15 | 16 | - [`OAuth2Client`](/reference/oauth2/OAuth2Client) 17 | - [`OAuth2RequestError`](/reference/oauth2/OAuth2RequestError) 18 | 19 | ## Interfaces 20 | 21 | - [`TokenResponseBody`](/reference/oauth2/TokenResponseBody) 22 | 23 | ## Types 24 | 25 | - [`ResponseMode`](/reference/oauth2/ResponseMode) 26 | 27 | ## Example 28 | 29 | ```ts 30 | import { OAuth2Client } from "oslo/oauth2"; 31 | 32 | const authorizeEndpoint = "https://github.com/login/oauth/authorize"; 33 | const tokenEndpoint = "https://github.com/login/oauth/access_token"; 34 | 35 | const client = new OAuth2Client(clientId, authorizeEndpoint, tokenEndpoint, { 36 | redirectURI: "http://localhost:3000/login/github/callback" 37 | }); 38 | ``` 39 | 40 | ### Create an authorization URL 41 | 42 | ```ts 43 | import { generateState } from "oslo/oauth2"; 44 | 45 | const state = generateState(); 46 | 47 | const url = await client.createAuthorizationURL({ 48 | state, 49 | scopes: ["user:email"] 50 | }); 51 | ``` 52 | 53 | It also supports the PKCE flow: 54 | 55 | ```ts 56 | import { generateState, generateCodeVerifier } from "oslo/oauth2"; 57 | 58 | const state = generateState(); 59 | const codeVerifier = generateCodeVerifier(); 60 | 61 | // S256 method by default 62 | const url = await client.createAuthorizationURL({ 63 | state, 64 | scopes: ["user:email"], 65 | codeVerifier 66 | }); 67 | ``` 68 | 69 | ### Validate an authorization code 70 | 71 | By default [`OAuth2Client.validateAuthorizationCode()`](/reference/oauth2/OAuth2Client/validateAuthorizationCode) sends credentials with the HTTP basic auth scheme. 72 | 73 | ```ts 74 | import { OAuth2RequestError } from "oslo/oauth2"; 75 | 76 | if (!storedState || !state || storedState !== state) { 77 | // error 78 | } 79 | 80 | // ... 81 | 82 | try { 83 | const { accessToken, refreshToken } = await client.validateAuthorizationCode<{ 84 | refreshToken: string; 85 | }>(code, { 86 | credentials: clientSecret, 87 | authenticateWith: "request_body" 88 | }); 89 | } catch (e) { 90 | if (e instanceof OAuth2RequestError) { 91 | // see https://www.rfc-editor.org/rfc/rfc6749#section-5.2 92 | const { request, message, description } = e; 93 | } 94 | // unknown error 95 | } 96 | ``` 97 | 98 | This also supports the PKCE flow: 99 | 100 | ```ts 101 | await client.validateAuthorizationCode<{ 102 | refreshToken: string; 103 | }>(code, { 104 | credentials: clientSecret, 105 | codeVerifier 106 | }); 107 | ``` 108 | 109 | ### Refresh an access token 110 | 111 | ```ts 112 | import { OAuth2RequestError } from "oslo/oauth2"; 113 | 114 | try { 115 | const { accessToken, refreshToken } = await client.refreshAccessToken<{ 116 | refreshToken: string; 117 | }>(code, { 118 | credentials: clientSecret, 119 | authenticateWith: "request_body" 120 | }); 121 | } catch (e) { 122 | if (e instanceof OAuth2RequestError) { 123 | // see https://www.rfc-editor.org/rfc/rfc6749#section-5.2 124 | const { request, message, description } = e; 125 | } 126 | // unknown error 127 | } 128 | ``` 129 | -------------------------------------------------------------------------------- /docs/pages/reference/otp/TOTPController/generate.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "TOTPController.generate()" 3 | --- 4 | 5 | # `TOTPController.generate()` 6 | 7 | Generates a new TOTP. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function generate(secretKey: ArrayBuffer | TypedArray): Promise; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `secretKey`: HMAC SHA-1 secret key 18 | -------------------------------------------------------------------------------- /docs/pages/reference/otp/TOTPController/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "TOTPController" 3 | --- 4 | 5 | # `TOTPController` 6 | 7 | Helper for Time-based OTP, as defined in [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238). Only supports HMAC SHA-1. 8 | 9 | ## Constructor 10 | 11 | ```ts 12 | //$ TimeSpan=/reference/main/TimeSpan 13 | function constructor(options?: { digits?: number; period?: $$TimeSpan }): this; 14 | ``` 15 | 16 | ### Parameters 17 | 18 | - `options` 19 | - `digits` (default: `6`): Number of digits (usually 6~8) 20 | - `period` (default: `30s`): How long the OTP is valid for at max 21 | 22 | ## Methods 23 | 24 | - [`generate()`](/reference/otp/TOTPController/generate) 25 | - [`verify()`](/reference/otp/TOTPController/verify) 26 | 27 | ## Example 28 | 29 | The secret key should be for HMAC SHA-1. 30 | 31 | ```ts 32 | import { TOTPController } from "oslo/otp"; 33 | import { TimeSpan } from "oslo"; 34 | import { HMAC } from "oslo/crypto"; 35 | 36 | const totpController = new TOTPController(); 37 | 38 | const secret = await new HMAC("SHA-1").generateKey(); 39 | 40 | const otp = await totpController.generate(secret); 41 | const validOTP = await totpController.verify(otp, secret); 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/pages/reference/otp/TOTPController/verify.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "TOTPController.verify()" 3 | --- 4 | 5 | # `TOTPController.verify()` 6 | 7 | Verifies the TOTP. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function verify(secretKey: ArrayBuffer | TypedArray, totp: string): Promise; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `secretKey`: HMAC secret key 18 | - `totp`: TOTP 19 | -------------------------------------------------------------------------------- /docs/pages/reference/otp/createHOTPKeyURI.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "createHOTPKeyURI()" 3 | --- 4 | 5 | # `createHOTPKeyURI()` 6 | 7 | Creates a new [key URI](https://github.com/google/google-authenticator/wiki/Key-Uri-Format) for HOTP. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function createHOTPKeyURI( 13 | issuer: string, 14 | accountName: string, 15 | secret: ArrayBuffer | TypedArray, 16 | options?: { 17 | counter?: number; 18 | digits?: number; 19 | } 20 | ): string; 21 | ``` 22 | 23 | ### Parameters 24 | 25 | - `issuer`: Your company/website name 26 | - `accountName`: Account identifier (e.g. username) 27 | - `secret`: HOTP secret key 28 | - `options` 29 | - `counter` (default: `0`): Counter count 30 | - `digits` (default: `6`): OTP digits 31 | 32 | ## Example 33 | 34 | ```ts 35 | import { createHOTPKeyURI } from "oslo/otp"; 36 | import { HMAC } from "oslo/crypto"; 37 | 38 | const secret = await new HMAC("SHA-1").generateKey(); 39 | 40 | const issuer = "My website"; 41 | const accountName = "user@example.com"; 42 | 43 | const uri = createHOTPKeyURI(issuer, accountName, secret); 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/pages/reference/otp/createTOTPKeyURI.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "createTOTPKeyURI()" 3 | --- 4 | 5 | # `createTOTPKeyURI()` 6 | 7 | Creates a new [key URI](https://github.com/google/google-authenticator/wiki/Key-Uri-Format) for TOTP. Only supports HMAC SHA-1. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ TimeSpan=/reference/main/TimeSpan 13 | function createTOTPKeyURI( 14 | issuer: string, 15 | accountName: string, 16 | secret: ArrayBuffer | TypedArray, 17 | options?: { 18 | digits?: number; 19 | period?: $$TimeSpan; 20 | } 21 | ): string; 22 | ``` 23 | 24 | ### Parameters 25 | 26 | - `issuer`: Your company/website name 27 | - `accountName`: Account identifier (e.g. username) 28 | - `secret`: TOTP secret key 29 | - `options` 30 | - `digits` (default: `6`): OTP digits 31 | - `period` (default: `30s`): How long the OTP is valid for at max 32 | 33 | ## Example 34 | 35 | ```ts 36 | import { createTOTPKeyURI } from "oslo/otp"; 37 | import { HMAC } from "oslo/crypto"; 38 | 39 | const secret = await new HMAC("SHA-1").generateKey(); 40 | 41 | const issuer = "My website"; 42 | const accountName = "user@example.com"; 43 | 44 | const uri = createTOTPKeyURI(issuer, accountName, secret); 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/pages/reference/otp/generateHOTP.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "generateHOTP()" 3 | --- 4 | 5 | # `generateHOTP()` 6 | 7 | Generates a new HOTP, as defined in [RFC 4226](https://www.ietf.org/rfc/rfc4226.txt). 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function generateHOTP( 13 | secretKey: ArrayBuffer | TypedArray, 14 | counter: number, 15 | digits?: number 16 | ): Promise; 17 | ``` 18 | 19 | ### Parameters 20 | 21 | - `secretKey`: HMAC SHA-1 secret key 22 | - `counter` 23 | - `digits` (default: `6`) 24 | 25 | ## Example 26 | 27 | ```ts 28 | import { generateHOTP } from "oslo/otp"; 29 | import { HMAC } from "oslo/crypto"; 30 | 31 | const secret = await new HMAC("SHA-1").generateKey(); 32 | 33 | let counter = 0; 34 | 35 | const otp = await generateHOTP(secret, counter); 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/pages/reference/otp/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "oslo/otp" 3 | --- 4 | 5 | # `oslo/otp` 6 | 7 | Provides utilities for working with HOTP (HMAC-based one-time password) and TOTP (Time-based one-time password). 8 | 9 | ## Functions 10 | 11 | - [`createHOTPKeyURI()`](/reference/otp/createHOTPKeyURI) 12 | - [`createTOTPKeyURI()`](/reference/otp/createTOTPKeyURI) 13 | - [`generateHOTP()`](/reference/otp/generateHOTP) 14 | 15 | ## Classes 16 | 17 | - [`TOTPController`](/reference/otp/TOTPController) 18 | -------------------------------------------------------------------------------- /docs/pages/reference/password/Argon2id/hash.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Argon2id.hash()" 3 | --- 4 | 5 | # `Argon2id.hash()` 6 | 7 | Hashes the provided password with argon2id. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function hash(password: string): Promise; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `password` 18 | 19 | ## Example 20 | 21 | ```ts 22 | //$ argon2id=/reference/password/Argon2id 23 | const hash = await $$argon2id.hash(password); 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/pages/reference/password/Argon2id/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Argon2id" 3 | --- 4 | 5 | # `Argon2id` 6 | 7 | Provides methods for hashing passwords and verifying hashes with [argon2id](https://datatracker.ietf.org/doc/rfc9106/). By default, the configuration is set to [the recommended values](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html). 8 | 9 | Implements [`PasswordHashingAlgorithm`](/reference/password/PasswordHashingAlgorithm). 10 | 11 | ## Constructor 12 | 13 | ```ts 14 | function constructor(options?: { 15 | memorySize?: number; 16 | iterations?: number; 17 | tagLength?: number; 18 | parallelism?: number; 19 | secret?: ArrayBuffer | TypedArray; 20 | }): this; 21 | ``` 22 | 23 | ### Parameters 24 | 25 | - `options` 26 | - `memorySize` (default: `19456`) 27 | - `iterations` (default: `2`) 28 | - `tagLength` (default: `32`) 29 | - `parallelism` (default: `1`) 30 | - `secret` 31 | 32 | ## Methods 33 | 34 | - [`hash()`](/reference/password/Argon2id/hash) 35 | - [`verify()`](/reference/password/Argon2id/verify) 36 | 37 | ## Example 38 | 39 | ```ts 40 | import { Argon2id } from "oslo/password"; 41 | 42 | const argon2id = new Argon2id(); 43 | const hash = await argon2id.hash(password); 44 | const validPassword = await argon2id.verify(hash, password); 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/pages/reference/password/Argon2id/verify.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Argon2id.verify()" 3 | --- 4 | 5 | # `Argon2id.verify()` 6 | 7 | Verifies the password with the hash using argon2id. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function verify(hash: string, password: string): Promise; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `hash` 18 | - `password` 19 | 20 | ## Example 21 | 22 | ```ts 23 | //$ argon2id=/reference/password/Argon2id 24 | const validPassword = await $$argon2id.verify(hash, password); 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/pages/reference/password/Bcrypt/hash.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Bcrypt.hash()" 3 | --- 4 | 5 | # `Bcrypt.hash()` 6 | 7 | Hashes the provided password with bcrypt. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function hash(password: string): Promise; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `password` 18 | 19 | ## Example 20 | 21 | ```ts 22 | //$ bcrypt=/reference/password/Bcrypt 23 | const hash = await $$bcrypt.hash(password); 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/pages/reference/password/Bcrypt/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Bcrypt" 3 | --- 4 | 5 | # `Bcrypt` 6 | 7 | Provides methods for hashing passwords and verifying hashes with [bcrypt](https://datatracker.ietf.org/doc/html/rfc7914). By default, the configuration is set to [the recommended values](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html). 8 | 9 | **We recommend using [`Argon2id`](/reference/password/Argon2id) or [`Scrypt`](/reference/password/Scrypt) if possible.** 10 | 11 | Implements [`PasswordHashingAlgorithm`](/reference/password/PasswordHashingAlgorithm). 12 | 13 | ## Constructor 14 | 15 | ```ts 16 | function constructor(options?: { cost?: number }): this; 17 | ``` 18 | 19 | ### Parameters 20 | 21 | - `options` 22 | - `cost` (default: `10`) 23 | 24 | ## Methods 25 | 26 | - [`hash()`](/reference/password/Argon2id/hash) 27 | - [`verify()`](/reference/password/Argon2id/verify) 28 | 29 | ## Example 30 | 31 | ```ts 32 | import { Bcrypt } from "oslo/password"; 33 | 34 | const bcrypt = new Bcrypt(); 35 | const hash = await bcrypt.hash(password); 36 | const validPassword = await bcrypt.verify(hash, password); 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/pages/reference/password/Bcrypt/verify.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Bcrypt.verify()" 3 | --- 4 | 5 | # `Bcrypt.verify()` 6 | 7 | Verifies the password with the hash using bcrypt. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function verify(hash: string, password: string): Promise; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `hash` 18 | - `password` 19 | 20 | ## Example 21 | 22 | ```ts 23 | //$ bcrypt=/reference/password/Bcrypt 24 | const validPassword = await $$bcrypt.verify(hash, password); 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/pages/reference/password/PasswordHashingAlgorithm.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "PasswordHashingAlgorithm" 3 | --- 4 | 5 | # `PasswordHashingAlgorithm` 6 | 7 | A generic interface for hashing and verifying passwords. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | interface PasswordHashingAlgorithm { 13 | hash(password: string): Promise; 14 | verify(hash: string, password: string): Promise; 15 | } 16 | ``` 17 | 18 | ### Methods 19 | 20 | - `hash()` 21 | - `verify()` 22 | 23 | ## Implemented by 24 | 25 | - [`Argon2id`](/reference/password/Argon2id) 26 | - [`Bcrypt`](/reference/password/Bcrypt) 27 | - [`Scrypt`](/reference/password/Scrypt) 28 | -------------------------------------------------------------------------------- /docs/pages/reference/password/Scrypt/hash.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Scrypt.hash()" 3 | --- 4 | 5 | # `Scrypt.hash()` 6 | 7 | Hashes the provided password with scrypt. The output hash is a combination of the scrypt hash and the 32-bytes salt, in the format of `:`. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function hash(password: string): Promise; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `password` 18 | 19 | ## Example 20 | 21 | ```ts 22 | //$ argon2id=/reference/password/scrypt 23 | const hash = await $$scrypt.hash(password); 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/pages/reference/password/Scrypt/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Scrypt" 3 | --- 4 | 5 | # `Scrypt` 6 | 7 | Provides methods for hashing passwords and verifying hashes with [scrypt](https://datatracker.ietf.org/doc/html/rfc7914). By default, the configuration is set to [the recommended values](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html). 8 | 9 | We recommend using [`Argon2id`](/reference/password/Argon2id) if possible. 10 | 11 | Implements [`PasswordHashingAlgorithm`](/reference/password/PasswordHashingAlgorithm). 12 | 13 | ## Constructor 14 | 15 | ```ts 16 | function constructor(options?: { N?: number; r?: number; p?: number; dkLen?: number }): this; 17 | ``` 18 | 19 | ### Parameters 20 | 21 | - `options` 22 | - `N` (default: `16384`) 23 | - `r` (default: `16`) 24 | - `p` (default: `1`) 25 | - `dkLen` (default: `64`) 26 | 27 | ## Methods 28 | 29 | - [`hash()`](/reference/password/Argon2id/hash) 30 | - [`verify()`](/reference/password/Argon2id/verify) 31 | 32 | ## Example 33 | 34 | ```ts 35 | import { Scrypt } from "oslo/password"; 36 | 37 | const scrypt = new Scrypt(); 38 | const hash = await scrypt.hash(password); 39 | const validPassword = await scrypt.verify(hash, password); 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/pages/reference/password/Scrypt/verify.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Scrypt.verify()" 3 | --- 4 | 5 | # `Scrypt.verify()` 6 | 7 | Verifies the password with the hash using scrypt. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function verify(hash: string, password: string): Promise; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `hash` 18 | - `password` 19 | 20 | ## Example 21 | 22 | ```ts 23 | //$ scrypt=/reference/password/Scrypt 24 | const validPassword = await $$scrypt.verify(hash, password); 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/pages/reference/password/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "oslo/password" 3 | --- 4 | 5 | # `oslo/password` 6 | 7 | **This module (and only this module) is NOT runtime agnostic and relies on Node-specific APIs.** 8 | 9 | Provides utilities for hashing passwords and verifying hashes. Argon2id is recommended, and if it's not possible, scrypt is recommended. 10 | 11 | ## Classes 12 | 13 | - [`Argon2id`](/reference/password/Argon2id) 14 | - [`Bcrypt`](/reference/password/Bcrypt) 15 | - [`Scrypt`](/reference/password/Scrypt) 16 | 17 | ## Interfaces 18 | 19 | - [`PasswordHashingAlgorithm`](/reference/password/PasswordHashingAlgorithm) 20 | 21 | ## Next.js 22 | 23 | In Next.js specifically, you must update your Webpack config to prevent dependencies from the getting bundled. 24 | 25 | ```ts 26 | // next.config.ts 27 | const nextConfig = { 28 | webpack: (config) => { 29 | config.externals.push("@node-rs/argon2", "@node-rs/bcrypt"); 30 | return config; 31 | } 32 | }; 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/pages/reference/request/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "oslo/request" 3 | --- 4 | 5 | # `oslo/request` 6 | 7 | Provides utilities for working with requests. 8 | 9 | ## Functions 10 | 11 | - [`verifyRequestOrigin()`](/reference/request/verifyRequestOrigin) 12 | -------------------------------------------------------------------------------- /docs/pages/reference/request/verifyRequestOrigin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "verifyRequestOrigin()" 3 | --- 4 | 5 | # `verifyRequestOrigin()` 6 | 7 | Verifies the request originates from a trusted origin by comparing the `Origin` header and host (e.g. `Host` header). 8 | 9 | ## Definition 10 | 11 | ```ts 12 | function verifyRequestOrigin(origin: string, allowedDomains: string[]): boolean; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `origin`: `Origin` header 18 | - `allowedDomains`: Allowed request origins, full URL or URL host 19 | 20 | ## Example 21 | 22 | ```ts 23 | import { verifyRequestOrigin } from "oslo/request"; 24 | 25 | // true 26 | verifyRequestOrigin("https://example.com", ["example.com"]); 27 | verifyRequestOrigin("https://example.com", ["https://example.com"]); 28 | 29 | // false 30 | verifyRequestOrigin("https://foo.example.com", ["example.com"]); 31 | verifyRequestOrigin("https://example.com", ["foo.example.com"]); 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/pages/reference/webauthn/AssertionResponse.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "AssertionResponse" 3 | --- 4 | 5 | # `AssertionResponse` 6 | 7 | Represents a WebAuthn assertion response. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | interface AssertionResponse { 13 | clientDataJSON: ArrayBuffer; 14 | authenticatorData: ArrayBuffer; 15 | signature: ArrayBuffer; 16 | } 17 | ``` 18 | 19 | ### Properties 20 | 21 | - `clientDataJSON` 22 | - `authenticatorData` 23 | - `signature` 24 | -------------------------------------------------------------------------------- /docs/pages/reference/webauthn/AttestationResponse.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "AttestationResponse" 3 | --- 4 | 5 | # `AttestationResponse` 6 | 7 | Represents a WebAuthn attestation response. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | interface AttestationResponse { 13 | clientDataJSON: ArrayBuffer; 14 | authenticatorData: ArrayBuffer; 15 | } 16 | ``` 17 | 18 | ### Properties 19 | 20 | - `clientDataJSON` 21 | - `authenticatorData` 22 | -------------------------------------------------------------------------------- /docs/pages/reference/webauthn/WebAuthnController/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "WebAuthnController" 3 | --- 4 | 5 | # `WebAuthnController` 6 | 7 | Provides methods for validating WebAuthn attestation and assertion responses. Supports ES256 (algorithm id `-7`) and RS256 (algorithm id `-257`). 8 | 9 | ## Constructor 10 | 11 | ```ts 12 | function constructor(origin: string): this; 13 | ``` 14 | 15 | ### Parameters 16 | 17 | - `origin`: Where the frontend is hosted (full url) 18 | 19 | ## Methods 20 | 21 | - [`validateAssertionResponse()`](/reference/webauthn/WebAuthnController/validateAssertionResponse) 22 | - [`validateAttestationResponse()`](/reference/webauthn/WebAuthnController/validateAttestationResponse) 23 | -------------------------------------------------------------------------------- /docs/pages/reference/webauthn/WebAuthnController/validateAssertionResponse.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "WebAuthnController.validateAssertionResponse()" 3 | --- 4 | 5 | # `WebAuthnController.validateAssertionResponse()` 6 | 7 | Validates a WebAuthn assertion response, including the signature. Supports ES256 (algorithm id `-7`) and RS256 (algorithm id `-257`). Throws an error on invalid response. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ AssertionResponse=/reference/webauthn/AssertionResponse 13 | function validateAssertionResponse( 14 | algorithm: "ES256" | "RS256", 15 | publicKey: ArrayBuffer | TypedArray, 16 | response: $$AssertionResponse, 17 | challenge: ArrayBuffer 18 | ): Promise; 19 | ``` 20 | 21 | ### Parameters 22 | 23 | - `algorithm`: Algorithm used for creating the signature 24 | - `publicKey`: Users's public key stored in the database 25 | - `response`: Attestation response 26 | - `challenge`: Challenge used for creating the signature 27 | 28 | ## Example 29 | 30 | ```ts 31 | //$ AssertionResponse=/reference/webauthn/AssertionResponse 32 | //$ webAuthnController=/reference/webauthn/WebAuthnController 33 | try { 34 | const response: $$AssertionResponse = { 35 | clientDataJSON, 36 | authenticatorData, 37 | signature 38 | }; 39 | await $$webAuthnController.validateAssertionResponse("ES256", publicKey, response, challenge); 40 | } catch { 41 | // failed to validate 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/pages/reference/webauthn/WebAuthnController/validateAttestationResponse.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "WebAuthnController.validateAssertionResponse()" 3 | --- 4 | 5 | # `WebAuthnController.validateAttestationResponse()` 6 | 7 | Validates a WebAuthn attestation response, including the signature, but not the attestation certificate. Throws an error on invalid response. 8 | 9 | ## Definition 10 | 11 | ```ts 12 | //$ AttestationResponse=/reference/webauthn/AttestationResponse 13 | function validateAttestationResponse( 14 | response: $$AttestationResponse, 15 | challenge: ArrayBuffer 16 | ): Promise; 17 | ``` 18 | 19 | ### Parameters 20 | 21 | - `response`: Attestation response 22 | - `challenge`: Challenge used for creating the signature 23 | 24 | ## Example 25 | 26 | ```ts 27 | //$ AttestationResponse=/reference/webauthn/AttestationResponse 28 | //$ webAuthnController=/reference/webauthn/WebAuthnController 29 | try { 30 | const response: $$AttestationResponse = { 31 | // all `ArrayBuffer` type (`Uint8Array`, `ArrayBuffer` etc) 32 | clientDataJSON, 33 | authenticatorData 34 | }; 35 | await $$webAuthnController.validateAttestationResponse(response, challenge); 36 | } catch { 37 | // failed to validate 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/pages/reference/webauthn/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "oslo/webauthn" 3 | --- 4 | 5 | # `oslo/webauthn` 6 | 7 | Provides utilities for working with Web Authentication, including Passkeys. 8 | 9 | ## Classes 10 | 11 | - [`WebAuthnController`](/reference/webauthn/WebAuthnController) 12 | 13 | ## Interfaces 14 | 15 | - [`AssertionResponse`](/reference/webauthn/AssertionResponse) 16 | - [`AttestationResponse`](/reference/webauthn/AttestationResponse) 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oslo", 3 | "type": "module", 4 | "version": "1.2.1", 5 | "description": "A collection of auth-related utilities", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "module": "dist/index.js", 9 | "scripts": { 10 | "build": "rm -rf dist/* && tsc --project tsconfig.build.json", 11 | "format": "prettier -w .", 12 | "lint": "eslint src", 13 | "test": "vitest run --sequence.concurrent" 14 | }, 15 | "files": [ 16 | "/dist/" 17 | ], 18 | "exports": { 19 | ".": "./dist/index.js", 20 | "./cookie": "./dist/cookie/index.js", 21 | "./crypto": "./dist/crypto/index.js", 22 | "./encoding": "./dist/encoding/index.js", 23 | "./jwt": "./dist/jwt/index.js", 24 | "./oauth2": "./dist/oauth2/index.js", 25 | "./otp": "./dist/otp/index.js", 26 | "./password": "./dist/password/index.js", 27 | "./request": "./dist/request/index.js", 28 | "./webauthn": "./dist/webauthn/index.js" 29 | }, 30 | "typesVersions": { 31 | "*": { 32 | ".": [ 33 | "dist/index.d.ts" 34 | ], 35 | "cookie": [ 36 | "dist/cookie/index.d.ts" 37 | ], 38 | "crypto": [ 39 | "dist/crypto/index.d.ts" 40 | ], 41 | "encoding": [ 42 | "dist/encoding/index.d.ts" 43 | ], 44 | "jwt": [ 45 | "dist/jwt/index.d.ts" 46 | ], 47 | "oauth2": [ 48 | "dist/oauth2/index.d.ts" 49 | ], 50 | "otp": [ 51 | "dist/otp/index.d.ts" 52 | ], 53 | "password": [ 54 | "dist/password/index.d.ts" 55 | ], 56 | "request": [ 57 | "dist/request/index.d.ts" 58 | ], 59 | "webauthn": [ 60 | "dist/webauthn/index.d.ts" 61 | ] 62 | } 63 | }, 64 | "keywords": [ 65 | "auth", 66 | "oauth2", 67 | "jwt", 68 | "crypto", 69 | "webauthn", 70 | "otp", 71 | "encoding", 72 | "auth", 73 | "random" 74 | ], 75 | "author": "pilcrowOnPaper", 76 | "license": "MIT", 77 | "repository": { 78 | "type": "git", 79 | "url": "https://github.com/pilcrowOnPaper/oslo" 80 | }, 81 | "devDependencies": { 82 | "@scure/base": "^1.1.3", 83 | "@types/node": "^20.8.6", 84 | "@typescript-eslint/eslint-plugin": "^6.7.5", 85 | "@typescript-eslint/parser": "^6.7.5", 86 | "auri": "1.0.2", 87 | "eslint": "^8.51.0", 88 | "prettier": "^3.0.3", 89 | "typescript": "^5.2.2", 90 | "vitest": "^0.34.6" 91 | }, 92 | "dependencies": { 93 | "@node-rs/argon2": "1.7.0", 94 | "@node-rs/bcrypt": "1.9.0" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/bytes.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | import { 3 | binaryToInteger, 4 | byteToBinary, 5 | bytesToBinary, 6 | bytesToInteger, 7 | compareBytes 8 | } from "./bytes.js"; 9 | 10 | test("bitsToInt()", () => { 11 | expect(binaryToInteger("110100101000010101")).toBe(215573); 12 | }); 13 | 14 | test("byteToBits()", () => { 15 | expect(byteToBinary(101)).toBe("01100101"); 16 | }); 17 | 18 | test("bytesToBits()", () => { 19 | expect(bytesToBinary(new Uint8Array([203, 3, 41, 76]))).toBe("11001011000000110010100101001100"); 20 | }); 21 | 22 | test("bytesToInteger()", () => { 23 | const bytes = Uint8Array.from([54, 204, 4, 128]); 24 | expect(bytesToInteger(bytes)).toBe(new DataView(bytes.buffer).getUint32(0)); 25 | }); 26 | 27 | test("compareBytes()", () => { 28 | const randomBytes = new Uint8Array(32); 29 | crypto.getRandomValues(randomBytes); 30 | expect(compareBytes(randomBytes, randomBytes)).toBe(true); 31 | const anotherRandomBytes = new Uint8Array(32); 32 | crypto.getRandomValues(anotherRandomBytes); 33 | expect(compareBytes(randomBytes, anotherRandomBytes)).toBe(false); 34 | }); 35 | -------------------------------------------------------------------------------- /src/bytes.ts: -------------------------------------------------------------------------------- 1 | import type { TypedArray } from "./index.js"; 2 | 3 | export function byteToBinary(byte: number): string { 4 | return byte.toString(2).padStart(8, "0"); 5 | } 6 | 7 | export function bytesToBinary(bytes: Uint8Array): string { 8 | return [...bytes].map((val) => byteToBinary(val)).join(""); 9 | } 10 | 11 | export function binaryToInteger(bits: string): number { 12 | return parseInt(bits, 2); 13 | } 14 | 15 | export function bytesToInteger(bytes: Uint8Array): number { 16 | return parseInt(bytesToBinary(bytes), 2); 17 | } 18 | 19 | export function compareBytes( 20 | buffer1: ArrayBuffer | TypedArray, 21 | buffer2: ArrayBuffer | TypedArray 22 | ): boolean { 23 | const bytes1 = new Uint8Array(buffer1); 24 | const bytes2 = new Uint8Array(buffer2); 25 | if (bytes1.byteLength !== bytes2.byteLength) return false; 26 | for (let i = 0; i < bytes1.byteLength; i++) { 27 | if (bytes1[i] !== bytes2[i]) return false; 28 | } 29 | return true; 30 | } 31 | -------------------------------------------------------------------------------- /src/cookie/index.test.ts: -------------------------------------------------------------------------------- 1 | import { serializeCookie, parseCookies } from "./index.js"; 2 | import { describe, test, expect } from "vitest"; 3 | 4 | describe("serializeCookie()", () => { 5 | test("serializes cookie", () => { 6 | const currDate = new Date(); 7 | const expected = `message=hello; Domain=example.com; Expires=${currDate.toUTCString()}; HttpOnly; Max-Age=60; Path=/foo; SameSite=Lax; Secure`; 8 | const result = serializeCookie("message", "hello", { 9 | domain: "example.com", 10 | expires: currDate, 11 | httpOnly: true, 12 | maxAge: 60, 13 | path: "/foo", 14 | sameSite: "lax", 15 | secure: true 16 | }); 17 | expect(result).toBe(expected); 18 | }); 19 | test("escapes name and value", () => { 20 | const expected = `%20-%5E%C2%A5%40%5B%3B%3A%5D%2C.%2F_!%22%23%24%25%26'()0%3D~%7C%60%7B%2B*%7D%3C%3E%3F%5C=%20-%5E%C2%A5%40%5B%3B%3A%5D%2C.%2F_!%22%23%24%25%26'()0%3D~%7C%60%7B%2B*%7D%3C%3E%3F%5C`; 21 | const result = serializeCookie( 22 | " -^¥@[;:],./_!\"#$%&'()0=~|`{+*}<>?\\", 23 | " -^¥@[;:],./_!\"#$%&'()0=~|`{+*}<>?\\", 24 | {} 25 | ); 26 | expect(result).toBe(expected); 27 | }); 28 | }); 29 | 30 | describe("parseCookies()", () => { 31 | test("parse cookie header", () => { 32 | const cookies = parseCookies("message1=hello; message2=bye"); 33 | expect(cookies.get("message1")).toBe("hello"); 34 | expect(cookies.get("message2")).toBe("bye"); 35 | expect(cookies.get("message3")).toBe(undefined); 36 | }); 37 | test("reads empty value", () => { 38 | const cookies = parseCookies("message1=; message2"); 39 | expect(cookies.get("message1")).toBe(""); 40 | expect(cookies.get("message2")).toBe(""); 41 | }); 42 | test("decodes escaped name and values", () => { 43 | const cookies = parseCookies( 44 | `%20-%5E%C2%A5%40%5B%3B%3A%5D%2C.%2F_!%22%23%24%25%26'()0%3D~%7C%60%7B%2B*%7D%3C%3E%3F%5C=%20-%5E%C2%A5%40%5B%3B%3A%5D%2C.%2F_!%22%23%24%25%26'()0%3D~%7C%60%7B%2B*%7D%3C%3E%3F%5C` 45 | ); 46 | expect(cookies.get(" -^¥@[;:],./_!\"#$%&'()0=~|`{+*}<>?\\")).toBe( 47 | " -^¥@[;:],./_!\"#$%&'()0=~|`{+*}<>?\\" 48 | ); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/cookie/index.ts: -------------------------------------------------------------------------------- 1 | import type { TimeSpan } from "../index.js"; 2 | 3 | export interface CookieAttributes { 4 | secure?: boolean; 5 | path?: string; 6 | domain?: string; 7 | sameSite?: "lax" | "strict" | "none"; 8 | httpOnly?: boolean; 9 | maxAge?: number; 10 | expires?: Date; 11 | } 12 | 13 | export function serializeCookie(name: string, value: string, attributes: CookieAttributes): string { 14 | const keyValueEntries: Array<[string, string] | [string]> = []; 15 | keyValueEntries.push([encodeURIComponent(name), encodeURIComponent(value)]); 16 | if (attributes?.domain !== undefined) { 17 | keyValueEntries.push(["Domain", attributes.domain]); 18 | } 19 | if (attributes?.expires !== undefined) { 20 | keyValueEntries.push(["Expires", attributes.expires.toUTCString()]); 21 | } 22 | if (attributes?.httpOnly) { 23 | keyValueEntries.push(["HttpOnly"]); 24 | } 25 | if (attributes?.maxAge !== undefined) { 26 | keyValueEntries.push(["Max-Age", attributes.maxAge.toString()]); 27 | } 28 | if (attributes?.path !== undefined) { 29 | keyValueEntries.push(["Path", attributes.path]); 30 | } 31 | if (attributes?.sameSite === "lax") { 32 | keyValueEntries.push(["SameSite", "Lax"]); 33 | } 34 | if (attributes?.sameSite === "none") { 35 | keyValueEntries.push(["SameSite", "None"]); 36 | } 37 | if (attributes?.sameSite === "strict") { 38 | keyValueEntries.push(["SameSite", "Strict"]); 39 | } 40 | if (attributes?.secure) { 41 | keyValueEntries.push(["Secure"]); 42 | } 43 | return keyValueEntries.map((pair) => pair.join("=")).join("; "); 44 | } 45 | 46 | export function parseCookies(header: string): Map { 47 | const cookies = new Map(); 48 | const items = header.split("; "); 49 | for (const item of items) { 50 | const pair = item.split("="); 51 | const rawKey = pair[0]; 52 | const rawValue = pair[1] ?? ""; 53 | if (!rawKey) continue; 54 | cookies.set(decodeURIComponent(rawKey), decodeURIComponent(rawValue)); 55 | } 56 | return cookies; 57 | } 58 | 59 | export class CookieController { 60 | constructor( 61 | cookieName: string, 62 | baseCookieAttributes: CookieAttributes, 63 | cookieOptions?: { 64 | expiresIn?: TimeSpan; 65 | } 66 | ) { 67 | this.cookieName = cookieName; 68 | this.cookieExpiresIn = cookieOptions?.expiresIn ?? null; 69 | this.baseCookieAttributes = baseCookieAttributes; 70 | } 71 | 72 | public cookieName: string; 73 | 74 | private cookieExpiresIn: TimeSpan | null; 75 | private baseCookieAttributes: CookieAttributes; 76 | 77 | public createCookie(value: string): Cookie { 78 | return new Cookie(this.cookieName, value, { 79 | ...this.baseCookieAttributes, 80 | maxAge: this.cookieExpiresIn?.seconds() 81 | }); 82 | } 83 | 84 | public createBlankCookie(): Cookie { 85 | return new Cookie(this.cookieName, "", { 86 | ...this.baseCookieAttributes, 87 | maxAge: 0 88 | }); 89 | } 90 | 91 | public parse(header: string): string | null { 92 | const cookies = parseCookies(header); 93 | return cookies.get(this.cookieName) ?? null; 94 | } 95 | } 96 | 97 | export class Cookie { 98 | constructor(name: string, value: string, attributes: CookieAttributes) { 99 | this.name = name; 100 | this.value = value; 101 | this.attributes = attributes; 102 | } 103 | 104 | public name: string; 105 | public value: string; 106 | public attributes: CookieAttributes; 107 | 108 | public serialize(): string { 109 | return serializeCookie(this.name, this.value, this.attributes); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/crypto/buffer.ts: -------------------------------------------------------------------------------- 1 | import type { TypedArray } from "../index.js"; 2 | 3 | export function constantTimeEqual( 4 | a: ArrayBuffer | TypedArray, 5 | b: ArrayBuffer | TypedArray 6 | ): boolean { 7 | const aBuffer = new Uint8Array(a); 8 | const bBuffer = new Uint8Array(b); 9 | if (aBuffer.length !== bBuffer.length) { 10 | return false; 11 | } 12 | let c = 0; 13 | for (let i = 0; i < aBuffer.length; i++) { 14 | c |= aBuffer[i]! ^ bBuffer[i]!; // ^: XOR operator 15 | } 16 | return c === 0; 17 | } 18 | -------------------------------------------------------------------------------- /src/crypto/ecdsa.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "vitest"; 2 | import { ECDSA } from "./index.js"; 3 | 4 | import type { ECDSACurve } from "./ecdsa.js"; 5 | import type { SHAHash } from "./sha.js"; 6 | 7 | interface TestCase { 8 | hash: SHAHash; 9 | curve: ECDSACurve; 10 | } 11 | 12 | const testCases: TestCase[] = [ 13 | { 14 | hash: "SHA-1", 15 | curve: "P-256" 16 | }, 17 | { 18 | hash: "SHA-256", 19 | curve: "P-256" 20 | }, 21 | { 22 | hash: "SHA-384", 23 | curve: "P-384" 24 | }, 25 | { 26 | hash: "SHA-512", 27 | curve: "P-521" 28 | } 29 | ]; 30 | 31 | describe.each(testCases)("ECDSA($hash, $curve)", ({ hash, curve }) => { 32 | test("Creates and verifies signature", async () => { 33 | const ecdsa = new ECDSA(hash, curve); 34 | const data = new TextEncoder().encode("Hello world!"); 35 | const { publicKey, privateKey } = await ecdsa.generateKeyPair(); 36 | const signature = await ecdsa.sign(privateKey, data); 37 | await expect(ecdsa.verify(publicKey, signature, data)).resolves.toBe(true); 38 | }); 39 | test("Fails on invalid signature", async () => { 40 | const ecdsa = new ECDSA(hash, curve); 41 | const data = new TextEncoder().encode("Hello world!"); 42 | const keyPairA = await ecdsa.generateKeyPair(); 43 | const signature = await ecdsa.sign(keyPairA.privateKey, data); 44 | const keyPairB = await ecdsa.generateKeyPair(); 45 | await expect(ecdsa.verify(keyPairB.publicKey, signature, data)).resolves.toBe(false); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/crypto/ecdsa.ts: -------------------------------------------------------------------------------- 1 | import type { TypedArray } from "../index.js"; 2 | import type { KeyPair } from "./index.js"; 3 | import type { SHAHash } from "./sha.js"; 4 | 5 | export type ECDSACurve = "P-256" | "P-384" | "P-521"; 6 | 7 | export class ECDSA { 8 | private hash: SHAHash; 9 | private curve: ECDSACurve; 10 | 11 | constructor(hash: SHAHash, curve: ECDSACurve) { 12 | this.hash = hash; 13 | this.curve = curve; 14 | } 15 | 16 | public async sign( 17 | privateKey: ArrayBuffer | TypedArray, 18 | data: ArrayBuffer | TypedArray 19 | ): Promise { 20 | const cryptoKey = await crypto.subtle.importKey( 21 | "pkcs8", 22 | privateKey, 23 | { 24 | name: "ECDSA", 25 | namedCurve: this.curve 26 | }, 27 | false, 28 | ["sign"] 29 | ); 30 | const signature = await crypto.subtle.sign( 31 | { 32 | name: "ECDSA", 33 | hash: this.hash 34 | }, 35 | cryptoKey, 36 | data 37 | ); 38 | return signature; 39 | } 40 | 41 | public async verify( 42 | publicKey: ArrayBuffer | TypedArray, 43 | signature: ArrayBuffer | TypedArray, 44 | data: ArrayBuffer | TypedArray 45 | ): Promise { 46 | const cryptoKey = await crypto.subtle.importKey( 47 | "spki", 48 | publicKey, 49 | { 50 | name: "ECDSA", 51 | namedCurve: this.curve 52 | }, 53 | false, 54 | ["verify"] 55 | ); 56 | return await crypto.subtle.verify( 57 | { 58 | name: "ECDSA", 59 | hash: this.hash 60 | }, 61 | cryptoKey, 62 | signature, 63 | data 64 | ); 65 | } 66 | 67 | public async generateKeyPair(): Promise { 68 | const cryptoKeyPair = await crypto.subtle.generateKey( 69 | { 70 | name: "ECDSA", 71 | namedCurve: this.curve 72 | }, 73 | true, 74 | ["sign"] 75 | ); 76 | const privateKey = await crypto.subtle.exportKey("pkcs8", cryptoKeyPair.privateKey); 77 | const publicKey = await crypto.subtle.exportKey("spki", cryptoKeyPair.publicKey); 78 | return { 79 | privateKey, 80 | publicKey 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/crypto/hmac.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "vitest"; 2 | import { HMAC } from "./index.js"; 3 | 4 | import type { SHAHash } from "./sha.js"; 5 | 6 | interface TestCase { 7 | hash: SHAHash; 8 | } 9 | 10 | const testCases: TestCase[] = [ 11 | { 12 | hash: "SHA-1" 13 | }, 14 | { 15 | hash: "SHA-256" 16 | }, 17 | { 18 | hash: "SHA-384" 19 | }, 20 | { 21 | hash: "SHA-512" 22 | } 23 | ]; 24 | 25 | describe.each(testCases)("HMAC($hash)", ({ hash }) => { 26 | test("Creates and verifies signature", async () => { 27 | const hmac = new HMAC(hash); 28 | const data = new TextEncoder().encode("Hello world!"); 29 | const key = await hmac.generateKey(); 30 | const signature = await hmac.sign(key, data); 31 | await expect(hmac.verify(key, signature, data)).resolves.toBe(true); 32 | }); 33 | test("Fails on invalid signature", async () => { 34 | const hmac = new HMAC(hash); 35 | const data = new TextEncoder().encode("Hello world!"); 36 | const keyA = await hmac.generateKey(); 37 | const signature = await hmac.sign(keyA, data); 38 | const keyB = await hmac.generateKey(); 39 | await expect(hmac.verify(keyB, signature, data)).resolves.toBe(false); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/crypto/hmac.ts: -------------------------------------------------------------------------------- 1 | import type { TypedArray } from "../index.js"; 2 | import type { SHAHash } from "./sha.js"; 3 | 4 | export class HMAC { 5 | private hash: SHAHash; 6 | constructor(hash: SHAHash) { 7 | this.hash = hash; 8 | } 9 | 10 | public async verify( 11 | key: ArrayBuffer | TypedArray, 12 | signature: ArrayBuffer | TypedArray, 13 | data: ArrayBuffer | TypedArray 14 | ): Promise { 15 | const cryptoKey = await crypto.subtle.importKey( 16 | "raw", 17 | key, 18 | { 19 | name: "HMAC", 20 | hash: this.hash 21 | }, 22 | false, 23 | ["verify"] 24 | ); 25 | return await crypto.subtle.verify("HMAC", cryptoKey, signature, data); 26 | } 27 | 28 | public async sign( 29 | key: ArrayBuffer | TypedArray, 30 | data: ArrayBuffer | TypedArray 31 | ): Promise { 32 | const cryptoKey = await crypto.subtle.importKey( 33 | "raw", 34 | key, 35 | { 36 | name: "HMAC", 37 | hash: this.hash 38 | }, 39 | false, 40 | ["sign"] 41 | ); 42 | const signature = await crypto.subtle.sign("HMAC", cryptoKey, data); 43 | return signature; 44 | } 45 | 46 | public async generateKey(): Promise { 47 | const cryptoKey: CryptoKey = await crypto.subtle.generateKey( 48 | { 49 | name: "HMAC", 50 | hash: this.hash 51 | }, 52 | true, 53 | ["sign"] 54 | ); 55 | const key = await crypto.subtle.exportKey("raw", cryptoKey); 56 | return key; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/crypto/index.ts: -------------------------------------------------------------------------------- 1 | export { ECDSA } from "./ecdsa.js"; 2 | export { HMAC } from "./hmac.js"; 3 | export { RSASSAPKCS1v1_5, RSASSAPSS } from "./rsa.js"; 4 | export { sha1, sha256, sha384, sha512 } from "./sha.js"; 5 | export { random, generateRandomInteger, generateRandomString, alphabet } from "./random.js"; 6 | export { constantTimeEqual } from "./buffer.js"; 7 | 8 | export type { ECDSACurve } from "./ecdsa.js"; 9 | export type { SHAHash } from "./sha.js"; 10 | 11 | import type { TypedArray } from "../index.js"; 12 | 13 | export interface KeyPair { 14 | publicKey: ArrayBuffer | TypedArray; 15 | privateKey: ArrayBuffer | TypedArray; 16 | } 17 | -------------------------------------------------------------------------------- /src/crypto/random.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { alphabet } from "./random.js"; 3 | 4 | test("alphabet()", async () => { 5 | expect(alphabet("0-9", "a-z", "A-Z", "-", "_")).toBe( 6 | "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_" 7 | ); 8 | }); 9 | -------------------------------------------------------------------------------- /src/crypto/random.ts: -------------------------------------------------------------------------------- 1 | import { bytesToInteger } from "../bytes.js"; 2 | 3 | export function random(): number { 4 | const buffer = new ArrayBuffer(8); 5 | const bytes = crypto.getRandomValues(new Uint8Array(buffer)); 6 | 7 | // sets the exponent value (11 bits) to 01111111111 (1023) 8 | // since the bias is 1023 (2 * (11 - 1) - 1), 1023 - 1023 = 0 9 | // 2^0 * (1 + [52 bit number between 0-1]) = number between 1-2 10 | bytes[0] = 63; 11 | bytes[1] = bytes[1]! | 240; 12 | 13 | return new DataView(buffer).getFloat64(0) - 1; 14 | } 15 | 16 | export function generateRandomInteger(max: number): number { 17 | if (max < 0 || !Number.isInteger(max)) { 18 | throw new Error("Argument 'max' must be an integer greater than or equal to 0"); 19 | } 20 | const bitLength = (max - 1).toString(2).length; 21 | const shift = bitLength % 8; 22 | const bytes = new Uint8Array(Math.ceil(bitLength / 8)); 23 | 24 | crypto.getRandomValues(bytes); 25 | 26 | // This zeroes bits that can be ignored to increase the chance `result` < `max`. 27 | // For example, if `max` can be represented with 10 bits, the leading 6 bits of the random 16 bits (2 bytes) can be ignored. 28 | if (shift !== 0) { 29 | bytes[0] &= (1 << shift) - 1; 30 | } 31 | let result = bytesToInteger(bytes); 32 | while (result >= max) { 33 | crypto.getRandomValues(bytes); 34 | if (shift !== 0) { 35 | bytes[0] &= (1 << shift) - 1; 36 | } 37 | result = bytesToInteger(bytes); 38 | } 39 | return result; 40 | } 41 | 42 | export function generateRandomString(length: number, alphabet: string): string { 43 | let result = ""; 44 | for (let i = 0; i < length; i++) { 45 | result += alphabet[generateRandomInteger(alphabet.length)]; 46 | } 47 | return result; 48 | } 49 | 50 | type AlphabetPattern = "a-z" | "A-Z" | "0-9" | "-" | "_"; 51 | 52 | export function alphabet(...patterns: AlphabetPattern[]): string { 53 | const patternSet = new Set(patterns); 54 | let result = ""; 55 | for (const pattern of patternSet) { 56 | if (pattern === "a-z") { 57 | result += "abcdefghijklmnopqrstuvwxyz"; 58 | } else if (pattern === "A-Z") { 59 | result += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 60 | } else if (pattern === "0-9") { 61 | result += "0123456789"; 62 | } else { 63 | result += pattern; 64 | } 65 | } 66 | return result; 67 | } 68 | -------------------------------------------------------------------------------- /src/crypto/rsa.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "vitest"; 2 | import { RSASSAPKCS1v1_5, RSASSAPSS } from "./rsa.js"; 3 | 4 | import type { SHAHash } from "./sha.js"; 5 | 6 | interface TestCase { 7 | hash: SHAHash; 8 | } 9 | 10 | const testCases: TestCase[] = [ 11 | { 12 | hash: "SHA-1" 13 | }, 14 | { 15 | hash: "SHA-256" 16 | }, 17 | { 18 | hash: "SHA-384" 19 | }, 20 | { 21 | hash: "SHA-512" 22 | } 23 | ]; 24 | 25 | describe.each(testCases)("RSASSAPKCS1v1_5($hash)", ({ hash }) => { 26 | test("Creates and verifies signature", async () => { 27 | const rsa = new RSASSAPKCS1v1_5(hash); 28 | const data = new TextEncoder().encode("Hello world!"); 29 | const { publicKey, privateKey } = await rsa.generateKeyPair(); 30 | const signature = await rsa.sign(privateKey, data); 31 | await expect(rsa.verify(publicKey, signature, data)).resolves.toBe(true); 32 | }); 33 | test("Fails on invalid signature", async () => { 34 | const rsa = new RSASSAPKCS1v1_5(hash); 35 | const data = new TextEncoder().encode("Hello world!"); 36 | const keyPairA = await rsa.generateKeyPair(); 37 | const signature = await rsa.sign(keyPairA.privateKey, data); 38 | const keyPairB = await rsa.generateKeyPair(); 39 | await expect(rsa.verify(keyPairB.publicKey, signature, data)).resolves.toBe(false); 40 | }); 41 | }); 42 | 43 | describe.each(testCases)("RSASSAPSS($hash)", ({ hash }) => { 44 | test("Creates and verifies signature", async () => { 45 | const rsa = new RSASSAPSS(hash); 46 | const data = new TextEncoder().encode("Hello world!"); 47 | const { publicKey, privateKey } = await rsa.generateKeyPair(); 48 | const signature = await rsa.sign(privateKey, data); 49 | await expect(rsa.verify(publicKey, signature, data)).resolves.toBe(true); 50 | }); 51 | test("Fails on invalid signature", async () => { 52 | const rsa = new RSASSAPKCS1v1_5(hash); 53 | const data = new TextEncoder().encode("Hello world!"); 54 | const keyPairA = await rsa.generateKeyPair(); 55 | const signature = await rsa.sign(keyPairA.privateKey, data); 56 | const keyPairB = await rsa.generateKeyPair(); 57 | await expect(rsa.verify(keyPairB.publicKey, signature, data)).resolves.toBe(false); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/crypto/rsa.ts: -------------------------------------------------------------------------------- 1 | import type { TypedArray } from "../index.js"; 2 | import type { KeyPair } from "./index.js"; 3 | import type { SHAHash } from "./sha.js"; 4 | 5 | export class RSASSAPKCS1v1_5 { 6 | private hash: SHAHash; 7 | constructor(hash: SHAHash) { 8 | this.hash = hash; 9 | } 10 | 11 | public async verify( 12 | publicKey: ArrayBuffer | TypedArray, 13 | signature: ArrayBuffer | TypedArray, 14 | data: ArrayBuffer | TypedArray 15 | ): Promise { 16 | const cryptoKey = await crypto.subtle.importKey( 17 | "spki", 18 | publicKey, 19 | { 20 | name: "RSASSA-PKCS1-v1_5", 21 | hash: this.hash 22 | }, 23 | false, 24 | ["verify"] 25 | ); 26 | return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", cryptoKey, signature, data); 27 | } 28 | 29 | public async sign( 30 | privateKey: ArrayBuffer | TypedArray, 31 | data: ArrayBuffer | TypedArray 32 | ): Promise { 33 | const cryptoKey = await crypto.subtle.importKey( 34 | "pkcs8", 35 | privateKey, 36 | { 37 | name: "RSASSA-PKCS1-v1_5", 38 | hash: this.hash 39 | }, 40 | false, 41 | ["sign"] 42 | ); 43 | const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", cryptoKey, data); 44 | return signature; 45 | } 46 | 47 | public async generateKeyPair(modulusLength?: 2048 | 4096): Promise { 48 | const cryptoKeyPair = await crypto.subtle.generateKey( 49 | { 50 | name: "RSASSA-PKCS1-v1_5", 51 | hash: this.hash, 52 | modulusLength: modulusLength ?? 2048, 53 | publicExponent: new Uint8Array([0x01, 0x00, 0x01]) 54 | }, 55 | true, 56 | ["sign"] 57 | ); 58 | const privateKey = await crypto.subtle.exportKey("pkcs8", cryptoKeyPair.privateKey); 59 | const publicKey = await crypto.subtle.exportKey("spki", cryptoKeyPair.publicKey); 60 | return { 61 | privateKey, 62 | publicKey 63 | }; 64 | } 65 | } 66 | 67 | export class RSASSAPSS { 68 | private hash: SHAHash; 69 | private saltLength: number; 70 | constructor(hash: SHAHash) { 71 | this.hash = hash; 72 | if (hash === "SHA-1") { 73 | this.saltLength = 20; 74 | } else if (hash === "SHA-256") { 75 | this.saltLength = 32; 76 | } else if (hash === "SHA-384") { 77 | this.saltLength = 48; 78 | } else { 79 | this.saltLength = 64; 80 | } 81 | } 82 | 83 | public async verify( 84 | publicKey: ArrayBuffer | TypedArray, 85 | signature: ArrayBuffer | TypedArray, 86 | data: ArrayBuffer | TypedArray 87 | ): Promise { 88 | const cryptoKey = await crypto.subtle.importKey( 89 | "spki", 90 | publicKey, 91 | { 92 | name: "RSA-PSS", 93 | hash: this.hash 94 | }, 95 | false, 96 | ["verify"] 97 | ); 98 | return await crypto.subtle.verify( 99 | { 100 | name: "RSA-PSS", 101 | saltLength: this.saltLength 102 | }, 103 | cryptoKey, 104 | signature, 105 | data 106 | ); 107 | } 108 | 109 | public async sign( 110 | privateKey: ArrayBuffer | TypedArray, 111 | data: ArrayBuffer | TypedArray 112 | ): Promise { 113 | const cryptoKey = await crypto.subtle.importKey( 114 | "pkcs8", 115 | privateKey, 116 | { 117 | name: "RSA-PSS", 118 | hash: this.hash 119 | }, 120 | false, 121 | ["sign"] 122 | ); 123 | const signature = await crypto.subtle.sign( 124 | { 125 | name: "RSA-PSS", 126 | saltLength: this.saltLength 127 | }, 128 | cryptoKey, 129 | data 130 | ); 131 | return signature; 132 | } 133 | 134 | public async generateKeyPair(modulusLength?: 2048 | 4096): Promise { 135 | const cryptoKeyPair = await crypto.subtle.generateKey( 136 | { 137 | name: "RSA-PSS", 138 | hash: this.hash, 139 | modulusLength: modulusLength ?? 2048, 140 | publicExponent: new Uint8Array([0x01, 0x00, 0x01]) 141 | }, 142 | true, 143 | ["sign"] 144 | ); 145 | const privateKey = await crypto.subtle.exportKey("pkcs8", cryptoKeyPair.privateKey); 146 | const publicKey = await crypto.subtle.exportKey("spki", cryptoKeyPair.publicKey); 147 | return { 148 | privateKey, 149 | publicKey 150 | }; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/crypto/sha.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { sha1, sha256, sha384, sha512 } from "./sha.js"; 3 | import { encodeHex } from "../encoding/index.js"; 4 | 5 | const data = new TextEncoder().encode("hello world"); 6 | 7 | test("sha1()", async () => { 8 | const result = await sha1(data); 9 | expect(encodeHex(result)).toBe("2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"); 10 | }); 11 | 12 | test("sha256()", async () => { 13 | const result = await sha256(data); 14 | expect(encodeHex(result)).toBe( 15 | "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" 16 | ); 17 | }); 18 | 19 | test("sha384()", async () => { 20 | const result = await sha384(data); 21 | expect(encodeHex(result)).toBe( 22 | "fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd" 23 | ); 24 | }); 25 | 26 | test("sha512()", async () => { 27 | const result = await sha512(data); 28 | expect(encodeHex(result)).toBe( 29 | "309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f" 30 | ); 31 | }); 32 | -------------------------------------------------------------------------------- /src/crypto/sha.ts: -------------------------------------------------------------------------------- 1 | import type { TypedArray } from "../index.js"; 2 | 3 | export async function sha1(data: ArrayBuffer | TypedArray): Promise { 4 | return await crypto.subtle.digest("SHA-1", data); 5 | } 6 | 7 | export async function sha256(data: ArrayBuffer | TypedArray): Promise { 8 | return await crypto.subtle.digest("SHA-256", data); 9 | } 10 | 11 | export async function sha384(data: ArrayBuffer | TypedArray): Promise { 12 | return await crypto.subtle.digest("SHA-384", data); 13 | } 14 | 15 | export async function sha512(data: ArrayBuffer | TypedArray): Promise { 16 | return await crypto.subtle.digest("SHA-512", data); 17 | } 18 | 19 | export type SHAHash = "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512"; 20 | -------------------------------------------------------------------------------- /src/encoding/base32.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { base32 as base32Reference } from "@scure/base"; 3 | import { base32 } from "./base32.js"; 4 | 5 | describe("Base32.encode()", () => { 6 | test("Generates valid string", () => { 7 | const cases = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 8 | for (const length of cases) { 9 | const data = crypto.getRandomValues(new Uint8Array(length)); 10 | expect(base32.encode(data)).toBe(base32Reference.encode(data)); 11 | } 12 | }); 13 | test("Omits padding", () => { 14 | const data = crypto.getRandomValues(new Uint8Array(4)); 15 | const result = base32.encode(data, { 16 | includePadding: false 17 | }); 18 | const expected = base32.encode(data).replaceAll("=", ""); 19 | expect(result).toBe(expected); 20 | }); 21 | }); 22 | 23 | describe("Base32.decode()", () => { 24 | test("Returns encoded data", () => { 25 | const cases = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 26 | for (const length of cases) { 27 | const data = crypto.getRandomValues(new Uint8Array(length)); 28 | const encoded = base32.encode(data); 29 | expect(base32.decode(encoded)).toStrictEqual(data); 30 | } 31 | }); 32 | test("Throws if data is missing padding in strict mode", () => { 33 | const data = crypto.getRandomValues(new Uint8Array(4)); 34 | const encoded = base32.encode(data, { 35 | includePadding: false 36 | }); 37 | expect(() => base32.decode(encoded.replaceAll("=", ""))).toThrow(); 38 | }); 39 | test("Accepts encoded data with missing padding if not in strict mode", () => { 40 | const data = crypto.getRandomValues(new Uint8Array(4)); 41 | const encoded = base32.encode(data, { 42 | includePadding: false 43 | }); 44 | const result = base32.decode(encoded.replaceAll("=", ""), { 45 | strict: false 46 | }); 47 | expect(result).toStrictEqual(data); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/encoding/base32.ts: -------------------------------------------------------------------------------- 1 | import type { TypedArray } from "../index.js"; 2 | import type { Encoding } from "./index.js"; 3 | 4 | export class Base32Encoding implements Encoding { 5 | public alphabet: string; 6 | public padding: string; 7 | 8 | private decodeMap = new Map(); 9 | 10 | constructor( 11 | alphabet: string, 12 | options?: { 13 | padding?: string; 14 | } 15 | ) { 16 | if (alphabet.length !== 32) { 17 | throw new Error("Invalid alphabet"); 18 | } 19 | this.alphabet = alphabet; 20 | this.padding = options?.padding ?? "="; 21 | if (this.alphabet.includes(this.padding) || this.padding.length !== 1) { 22 | throw new Error("Invalid padding"); 23 | } 24 | for (let i = 0; i < alphabet.length; i++) { 25 | this.decodeMap.set(alphabet[i]!, i); 26 | } 27 | } 28 | 29 | public encode( 30 | data: Uint8Array, 31 | options?: { 32 | includePadding?: boolean; 33 | } 34 | ): string { 35 | let result = ""; 36 | let buffer = 0; 37 | let shift = 0; 38 | for (let i = 0; i < data.length; i++) { 39 | buffer = (buffer << 8) | data[i]!; 40 | shift += 8; 41 | while (shift >= 5) { 42 | shift -= 5; 43 | result += this.alphabet[(buffer >> shift) & 0x1f]; 44 | } 45 | } 46 | if (shift > 0) { 47 | result += this.alphabet[(buffer << (5 - shift)) & 0x1f]; 48 | } 49 | const includePadding = options?.includePadding ?? true; 50 | if (includePadding) { 51 | const padCount = (8 - (result.length % 8)) % 8; 52 | for (let i = 0; i < padCount; i++) { 53 | result += "="; 54 | } 55 | } 56 | return result; 57 | } 58 | 59 | public decode( 60 | data: string, 61 | options?: { 62 | strict?: boolean; 63 | } 64 | ): Uint8Array { 65 | const strict = options?.strict ?? true; 66 | const chunkCount = Math.ceil(data.length / 8); 67 | const result: number[] = []; 68 | for (let i = 0; i < chunkCount; i++) { 69 | let padCount = 0; 70 | const chunks: number[] = []; 71 | for (let j = 0; j < 8; j++) { 72 | const encoded = data[i * 8 + j]; 73 | if (encoded === "=") { 74 | if (i + 1 !== chunkCount) { 75 | throw new Error(`Invalid character: ${encoded}`); 76 | } 77 | padCount += 1; 78 | continue; 79 | } 80 | if (encoded === undefined) { 81 | if (strict) { 82 | throw new Error("Invalid data"); 83 | } 84 | padCount += 1; 85 | continue; 86 | } 87 | const value = this.decodeMap.get(encoded) ?? null; 88 | if (value === null) { 89 | throw new Error(`Invalid character: ${encoded}`); 90 | } 91 | chunks.push(value); 92 | } 93 | if (padCount === 8 || padCount === 7 || padCount === 5 || padCount === 2) { 94 | throw new Error("Invalid padding"); 95 | } 96 | const byte1 = (chunks[0]! << 3) + (chunks[1]! >> 2); 97 | result.push(byte1); 98 | if (padCount < 6) { 99 | const byte2 = ((chunks[1]! & 0x03) << 6) + (chunks[2]! << 1) + (chunks[3]! >> 4); 100 | result.push(byte2); 101 | } 102 | if (padCount < 4) { 103 | const byte3 = ((chunks[3]! & 0xff) << 4) + (chunks[4]! >> 1); 104 | result.push(byte3); 105 | } 106 | if (padCount < 3) { 107 | const byte4 = ((chunks[4]! & 0x01) << 7) + (chunks[5]! << 2) + (chunks[6]! >> 3); 108 | result.push(byte4); 109 | } 110 | if (padCount < 1) { 111 | const byte5 = ((chunks[6]! & 0x07) << 5) + chunks[7]!; 112 | result.push(byte5); 113 | } 114 | } 115 | return Uint8Array.from(result); 116 | } 117 | } 118 | 119 | export const base32 = new Base32Encoding("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"); 120 | export const base32hex = new Base32Encoding("0123456789ABCDEFGHIJKLMNOPQRSTUV"); 121 | 122 | /** @deprecated Use `base32.encode()` instead */ 123 | export function encodeBase32( 124 | data: ArrayBuffer | TypedArray, 125 | options?: { 126 | padding?: boolean; 127 | } 128 | ): string { 129 | return base32.encode(new Uint8Array(data), { 130 | includePadding: options?.padding ?? true 131 | }); 132 | } 133 | 134 | /** @deprecated Use `base32.decode()` instead */ 135 | export function decodeBase32(data: string): Uint8Array { 136 | return base32.decode(data, { 137 | strict: false 138 | }); 139 | } 140 | -------------------------------------------------------------------------------- /src/encoding/base64.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { base64 } from "./base64.js"; 3 | 4 | describe("Base64.encode()", () => { 5 | test("Generates valid string", () => { 6 | const cases = [0, 1, 2, 3, 4, 5, 6]; 7 | for (const length of cases) { 8 | const data = crypto.getRandomValues(new Uint8Array(length)); 9 | expect(base64.encode(data)).toBe(Buffer.from(data).toString("base64")); 10 | } 11 | }); 12 | test("Omits padding", () => { 13 | const data = crypto.getRandomValues(new Uint8Array(4)); 14 | const result = base64.encode(data, { 15 | includePadding: false 16 | }); 17 | const expected = base64.encode(data).replaceAll("=", ""); 18 | expect(result).toBe(expected); 19 | }); 20 | }); 21 | 22 | describe("Base64.decode()", () => { 23 | test("Returns encoded data", () => { 24 | const cases = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 25 | for (const length of cases) { 26 | const data = crypto.getRandomValues(new Uint8Array(length)); 27 | const encoded = base64.encode(data); 28 | expect(base64.decode(encoded)).toStrictEqual(data); 29 | } 30 | }); 31 | test("Throws if data is missing padding in strict mode", () => { 32 | const data = crypto.getRandomValues(new Uint8Array(4)); 33 | const encoded = base64.encode(data, { 34 | includePadding: false 35 | }); 36 | expect(() => base64.decode(encoded.replaceAll("=", ""))).toThrow(); 37 | }); 38 | test("Accepts encoded data with missing padding if not in strict mode", () => { 39 | const data = crypto.getRandomValues(new Uint8Array(4)); 40 | const encoded = base64.encode(data, { 41 | includePadding: false 42 | }); 43 | const result = base64.decode(encoded.replaceAll("=", ""), { 44 | strict: false 45 | }); 46 | expect(result).toStrictEqual(data); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/encoding/base64.ts: -------------------------------------------------------------------------------- 1 | import type { TypedArray } from "../index.js"; 2 | import type { Encoding } from "./index.js"; 3 | 4 | export class Base64Encoding implements Encoding { 5 | public alphabet: string; 6 | public padding: string; 7 | 8 | private decodeMap = new Map(); 9 | 10 | constructor( 11 | alphabet: string, 12 | options?: { 13 | padding?: string; 14 | } 15 | ) { 16 | if (alphabet.length !== 64) { 17 | throw new Error("Invalid alphabet"); 18 | } 19 | this.alphabet = alphabet; 20 | this.padding = options?.padding ?? "="; 21 | if (this.alphabet.includes(this.padding) || this.padding.length !== 1) { 22 | throw new Error("Invalid padding"); 23 | } 24 | for (let i = 0; i < alphabet.length; i++) { 25 | this.decodeMap.set(alphabet[i]!, i); 26 | } 27 | } 28 | 29 | public encode( 30 | data: Uint8Array, 31 | options?: { 32 | includePadding?: boolean; 33 | } 34 | ): string { 35 | let result = ""; 36 | let buffer = 0; 37 | let shift = 0; 38 | for (let i = 0; i < data.length; i++) { 39 | buffer = (buffer << 8) | data[i]!; 40 | shift += 8; 41 | while (shift >= 6) { 42 | shift += -6; 43 | result += this.alphabet[(buffer >> shift) & 0x3f]; 44 | } 45 | } 46 | if (shift > 0) { 47 | result += this.alphabet[(buffer << (6 - shift)) & 0x3f]; 48 | } 49 | const includePadding = options?.includePadding ?? true; 50 | if (includePadding) { 51 | const padCount = (4 - (result.length % 4)) % 4; 52 | for (let i = 0; i < padCount; i++) { 53 | result += "="; 54 | } 55 | } 56 | return result; 57 | } 58 | 59 | public decode( 60 | data: string, 61 | options?: { 62 | strict?: boolean; 63 | } 64 | ): Uint8Array { 65 | const strict = options?.strict ?? true; 66 | const chunkCount = Math.ceil(data.length / 4); 67 | const result: number[] = []; 68 | for (let i = 0; i < chunkCount; i++) { 69 | let padCount = 0; 70 | let buffer = 0; 71 | for (let j = 0; j < 4; j++) { 72 | const encoded = data[i * 4 + j]; 73 | if (encoded === "=") { 74 | if (i + 1 !== chunkCount) { 75 | throw new Error(`Invalid character: ${encoded}`); 76 | } 77 | padCount += 1; 78 | continue; 79 | } 80 | if (encoded === undefined) { 81 | if (strict) { 82 | throw new Error("Invalid data"); 83 | } 84 | padCount += 1; 85 | continue; 86 | } 87 | const value = this.decodeMap.get(encoded) ?? null; 88 | if (value === null) { 89 | throw new Error(`Invalid character: ${encoded}`); 90 | } 91 | buffer += value << (6 * (3 - j)); 92 | } 93 | result.push((buffer >> 16) & 0xff); 94 | if (padCount < 2) { 95 | result.push((buffer >> 8) & 0xff); 96 | } 97 | if (padCount < 1) { 98 | result.push(buffer & 0xff); 99 | } 100 | } 101 | return Uint8Array.from(result); 102 | } 103 | } 104 | 105 | export const base64 = new Base64Encoding( 106 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 107 | ); 108 | 109 | export const base64url = new Base64Encoding( 110 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" 111 | ); 112 | 113 | /** @deprecated Use `base64.encode()` instead */ 114 | export function encodeBase64( 115 | data: ArrayBuffer | TypedArray, 116 | options?: { 117 | padding?: boolean; 118 | } 119 | ): string { 120 | return base64.encode(new Uint8Array(data), { 121 | includePadding: options?.padding ?? true 122 | }); 123 | } 124 | 125 | /** @deprecated Use `base64.decode()` instead */ 126 | export function decodeBase64(data: string): Uint8Array { 127 | return base64.decode(data, { 128 | strict: false 129 | }); 130 | } 131 | 132 | /** @deprecated Use `base64url.encode()` instead */ 133 | export function encodeBase64url(data: ArrayBuffer | TypedArray): string { 134 | return base64.encode(new Uint8Array(data), { 135 | includePadding: false 136 | }); 137 | } 138 | 139 | /** @deprecated Use `base64url.decode()` instead */ 140 | export function decodeBase64url(data: string): Uint8Array { 141 | return base64url.decode(data, { 142 | strict: false 143 | }); 144 | } 145 | -------------------------------------------------------------------------------- /src/encoding/hex.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { encodeHex, decodeHex } from "./hex.js"; 3 | 4 | describe("encodeHex()", () => { 5 | test("Generates valid hex string", () => { 6 | const cases = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 7 | for (const length of cases) { 8 | const data = crypto.getRandomValues(new Uint8Array(length)); 9 | expect(encodeHex(data)).toBe(Buffer.from(data).toString("hex")); 10 | } 11 | }); 12 | }); 13 | 14 | describe("Base32.decode()", () => { 15 | test("Returns encoded data", () => { 16 | const cases = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 17 | for (const length of cases) { 18 | const data = crypto.getRandomValues(new Uint8Array(length)); 19 | const encoded = encodeHex(data); 20 | expect(decodeHex(encoded)).toStrictEqual(data); 21 | expect(decodeHex(encoded.toUpperCase())).toStrictEqual(data); 22 | } 23 | }); 24 | test("Throws if data is invalid", () => { 25 | expect(() => decodeHex("a")).toThrow(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/encoding/hex.ts: -------------------------------------------------------------------------------- 1 | import type { TypedArray } from "../index.js"; 2 | 3 | const hexAlphabet = "0123456789abcdef"; 4 | 5 | const hexDecodeMap = new Map([ 6 | ["0", 0], 7 | ["1", 1], 8 | ["2", 2], 9 | ["3", 3], 10 | ["4", 4], 11 | ["5", 5], 12 | ["6", 6], 13 | ["7", 7], 14 | ["8", 8], 15 | ["9", 9], 16 | 17 | ["A", 10], 18 | ["B", 11], 19 | ["C", 12], 20 | ["D", 13], 21 | ["E", 14], 22 | ["F", 15], 23 | 24 | ["a", 10], 25 | ["b", 11], 26 | ["c", 12], 27 | ["d", 13], 28 | ["e", 14], 29 | ["f", 15] 30 | ]); 31 | 32 | export function encodeHex(data: ArrayBuffer | TypedArray): string { 33 | const bytes = new Uint8Array(data); 34 | let result = ""; 35 | for (let i = 0; i < bytes.length; i++) { 36 | const key1 = bytes[i]! >> 4; 37 | result += hexAlphabet[key1]; 38 | const key2 = bytes[i]! & 0x0f; 39 | result += hexAlphabet[key2]; 40 | } 41 | return result; 42 | } 43 | 44 | export function decodeHex(data: string): Uint8Array { 45 | const chunkCount = Math.ceil(data.length / 2); 46 | const result = new Uint8Array(chunkCount); 47 | for (let i = 0; i < chunkCount; i++) { 48 | let buffer = 0; 49 | 50 | const encoded1 = data[i * 2]!; 51 | const value1 = hexDecodeMap.get(encoded1) ?? null; 52 | if (value1 === null) { 53 | throw new Error(`Invalid character: ${encoded1}`); 54 | } 55 | buffer += value1 << 4; 56 | 57 | const encoded2 = data[i * 2 + 1]; 58 | if (encoded2 === undefined) { 59 | throw new Error("Invalid data"); 60 | } 61 | const value2 = hexDecodeMap.get(encoded2) ?? null; 62 | if (value2 === null) { 63 | throw new Error(`Invalid character: ${encoded1}`); 64 | } 65 | buffer += value2; 66 | 67 | result[i] = buffer; 68 | } 69 | return result; 70 | } 71 | -------------------------------------------------------------------------------- /src/encoding/index.ts: -------------------------------------------------------------------------------- 1 | export { encodeHex, decodeHex } from "./hex.js"; 2 | export { Base32Encoding, base32, base32hex } from "./base32.js"; 3 | export { Base64Encoding, base64, base64url } from "./base64.js"; 4 | 5 | export interface Encoding { 6 | encode: (data: Uint8Array) => string; 7 | decode: (data: string) => Uint8Array; 8 | } 9 | 10 | export { encodeBase32, decodeBase32 } from "./base32.js"; 11 | export { encodeBase64, encodeBase64url, decodeBase64, decodeBase64url } from "./base64.js"; 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type TimeSpanUnit = "ms" | "s" | "m" | "h" | "d" | "w"; 2 | 3 | export class TimeSpan { 4 | constructor(value: number, unit: TimeSpanUnit) { 5 | this.value = value; 6 | this.unit = unit; 7 | } 8 | 9 | public value: number; 10 | public unit: TimeSpanUnit; 11 | 12 | public milliseconds(): number { 13 | if (this.unit === "ms") { 14 | return this.value; 15 | } 16 | if (this.unit === "s") { 17 | return this.value * 1000; 18 | } 19 | if (this.unit === "m") { 20 | return this.value * 1000 * 60; 21 | } 22 | if (this.unit === "h") { 23 | return this.value * 1000 * 60 * 60; 24 | } 25 | if (this.unit === "d") { 26 | return this.value * 1000 * 60 * 60 * 24; 27 | } 28 | return this.value * 1000 * 60 * 60 * 24 * 7; 29 | } 30 | 31 | public seconds(): number { 32 | return this.milliseconds() / 1000; 33 | } 34 | 35 | public transform(x: number): TimeSpan { 36 | return new TimeSpan(Math.round(this.milliseconds() * x), "ms"); 37 | } 38 | } 39 | 40 | export function isWithinExpirationDate(date: Date): boolean { 41 | return Date.now() < date.getTime(); 42 | } 43 | 44 | export function createDate(timeSpan: TimeSpan): Date { 45 | return new Date(Date.now() + timeSpan.milliseconds()); 46 | } 47 | 48 | export type TypedArray = 49 | | Uint8Array 50 | | Int8Array 51 | | Uint16Array 52 | | Int16Array 53 | | Uint32Array 54 | | Int32Array 55 | | Float32Array 56 | | Float64Array 57 | | BigInt64Array 58 | | BigUint64Array; 59 | -------------------------------------------------------------------------------- /src/jwt/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "vitest"; 2 | import { createJWT, parseJWT, validateJWT } from "./index.js"; 3 | 4 | import { HMAC } from "../crypto/hmac.js"; 5 | import { ECDSA } from "../crypto/ecdsa.js"; 6 | import { RSASSAPKCS1v1_5, RSASSAPSS } from "../crypto/rsa.js"; 7 | import { TimeSpan } from "../index.js"; 8 | 9 | test.each(["ES256", "ES384", "ES512"] as const)( 10 | "Create and validate JWT with %s", 11 | async (algorithm) => { 12 | const { publicKey, privateKey } = await new ECDSA( 13 | ecdsaDictionary[algorithm].hash, 14 | ecdsaDictionary[algorithm].curve 15 | ).generateKeyPair(); 16 | const jwt = await createJWT(algorithm, privateKey, { 17 | message: "hello" 18 | }); 19 | const validatedJWT = await validateJWT(algorithm, publicKey, jwt); 20 | expect(validatedJWT.algorithm).toBe(algorithm); 21 | expect(validatedJWT.header).toStrictEqual({ 22 | typ: "JWT", 23 | alg: algorithm 24 | }); 25 | expect(validatedJWT.payload).toStrictEqual({ 26 | message: "hello" 27 | }); 28 | } 29 | ); 30 | 31 | test.each(["RS256", "RS384", "RS512"] as const)( 32 | "Create and validate JWT with %s", 33 | async (algorithm) => { 34 | const { publicKey, privateKey } = await new RSASSAPKCS1v1_5( 35 | rsassapkcs1v1_5Dictionary[algorithm] 36 | ).generateKeyPair(); 37 | const jwt = await createJWT(algorithm, privateKey, { 38 | message: "hello" 39 | }); 40 | const validatedJWT = await validateJWT(algorithm, publicKey, jwt); 41 | expect(validatedJWT.algorithm).toBe(algorithm); 42 | expect(validatedJWT.header).toStrictEqual({ 43 | typ: "JWT", 44 | alg: algorithm 45 | }); 46 | expect(validatedJWT.payload).toStrictEqual({ 47 | message: "hello" 48 | }); 49 | } 50 | ); 51 | 52 | test.each(["PS256", "PS384", "PS512"] as const)( 53 | "Create and validate JWT with %s", 54 | async (algorithm) => { 55 | const { publicKey, privateKey } = await new RSASSAPSS( 56 | rsassapssDictionary[algorithm] 57 | ).generateKeyPair(); 58 | const jwt = await createJWT(algorithm, privateKey, { 59 | message: "hello" 60 | }); 61 | const validatedJWT = await validateJWT(algorithm, publicKey, jwt); 62 | expect(validatedJWT.algorithm).toBe(algorithm); 63 | expect(validatedJWT.header).toStrictEqual({ 64 | typ: "JWT", 65 | alg: algorithm 66 | }); 67 | expect(validatedJWT.payload).toStrictEqual({ 68 | message: "hello" 69 | }); 70 | } 71 | ); 72 | 73 | test.each(["HS256", "HS384", "HS512"] as const)( 74 | "Create and validate JWT with %s", 75 | async (algorithm) => { 76 | const secretKey = await new HMAC(hmacDictionary[algorithm]).generateKey(); 77 | const jwt = await createJWT(algorithm, secretKey, { 78 | message: "hello" 79 | }); 80 | const validatedJWT = await validateJWT(algorithm, secretKey, jwt); 81 | expect(validatedJWT.algorithm).toBe(algorithm); 82 | expect(validatedJWT.header).toStrictEqual({ 83 | typ: "JWT", 84 | alg: algorithm 85 | }); 86 | expect(validatedJWT.payload).toStrictEqual({ 87 | message: "hello" 88 | }); 89 | } 90 | ); 91 | 92 | describe("createJWT()", () => { 93 | test("Creates the correct JWT value", async () => { 94 | const secretKey = new Uint8Array([ 95 | 8, 138, 53, 76, 210, 41, 194, 216, 13, 70, 56, 196, 237, 57, 69, 41, 152, 114, 223, 150, 169, 96 | 154, 191, 89, 202, 118, 249, 18, 34, 208, 18, 101, 70, 236, 76, 178, 117, 129, 106, 71, 253, 97 | 79, 99, 9, 64, 208, 102, 50, 118, 72, 107, 46, 120, 2, 240, 217, 103, 66, 63, 52, 248, 23, 98 | 140, 46 99 | ]); 100 | const result = await createJWT( 101 | "HS256", 102 | secretKey, 103 | { 104 | message: "hello", 105 | count: 100 106 | }, 107 | { 108 | audiences: ["_audience"], 109 | issuer: "_issuer", 110 | subject: "_subject", 111 | jwtId: "_jwtId" 112 | } 113 | ); 114 | const expected = 115 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8iLCJjb3VudCI6MTAwLCJhdWQiOlsiX2F1ZGllbmNlIl0sInN1YiI6Il9zdWJqZWN0IiwiaXNzIjoiX2lzc3VlciIsImp0aSI6Il9qd3RJZCJ9.cKi5L4ZV79IHtpC-rXRwjnQIeWdswAvv1KavDSM_vds"; 116 | expect(result).toBe(expected); 117 | }); 118 | }); 119 | 120 | test("parseJWT()", async () => { 121 | const secretKey = await new HMAC("SHA-256").generateKey(); 122 | const currDateSeconds = Math.floor(Date.now() / 1000); 123 | const jwt = await createJWT( 124 | "HS256", 125 | secretKey, 126 | { 127 | message: "hello" 128 | }, 129 | { 130 | audiences: ["_audience"], 131 | issuer: "_issuer", 132 | subject: "_subject", 133 | jwtId: "_jwtId", 134 | expiresIn: new TimeSpan(1, "h"), 135 | notBefore: new Date(), 136 | includeIssuedTimestamp: true, 137 | headers: { 138 | kid: "_kid" 139 | } 140 | } 141 | ); 142 | expect(parseJWT(jwt)).toEqual({ 143 | algorithm: "HS256", 144 | expiresAt: new Date((currDateSeconds + new TimeSpan(1, "h").seconds()) * 1000), 145 | notBefore: new Date(currDateSeconds * 1000), 146 | issuedAt: new Date(currDateSeconds * 1000), 147 | audiences: ["_audience"], 148 | issuer: "_issuer", 149 | subject: "_subject", 150 | jwtId: "_jwtId", 151 | value: jwt, 152 | parts: jwt.split("."), 153 | header: { 154 | kid: "_kid", 155 | typ: "JWT", 156 | alg: "HS256" 157 | }, 158 | payload: { 159 | message: "hello", 160 | aud: ["_audience"], 161 | iss: "_issuer", 162 | sub: "_subject", 163 | jti: "_jwtId", 164 | exp: currDateSeconds + new TimeSpan(1, "h").seconds(), 165 | iat: currDateSeconds, 166 | nbf: currDateSeconds 167 | } 168 | }); 169 | }); 170 | 171 | describe("validateJWT", () => { 172 | test("Checks expiration", async () => { 173 | const secretKey = await new HMAC("SHA-256").generateKey(); 174 | const jwt1 = await createJWT( 175 | "HS256", 176 | secretKey, 177 | {}, 178 | { 179 | expiresIn: new TimeSpan(-1, "s") 180 | } 181 | ); 182 | const jwt2 = await createJWT( 183 | "HS256", 184 | secretKey, 185 | {}, 186 | { 187 | expiresIn: new TimeSpan(0, "s") 188 | } 189 | ); 190 | await expect(validateJWT("HS256", secretKey, jwt1)).rejects.toThrowError(); 191 | await expect(validateJWT("HS256", secretKey, jwt2)).rejects.toThrowError(); 192 | }); 193 | test("Checks not before time", async () => { 194 | const secretKey = await new HMAC("SHA-256").generateKey(); 195 | const jwt1 = await createJWT( 196 | "HS256", 197 | secretKey, 198 | {}, 199 | { 200 | notBefore: new Date(Date.now() + 1000) 201 | } 202 | ); 203 | const jwt2 = await createJWT( 204 | "HS256", 205 | secretKey, 206 | {}, 207 | { 208 | notBefore: new Date() 209 | } 210 | ); 211 | await expect(validateJWT("HS256", secretKey, jwt1)).rejects.toThrowError(); 212 | await expect(validateJWT("HS256", secretKey, jwt2)).resolves.not.toThrowError(); 213 | }); 214 | test("Throws on invalid algorithm", async () => { 215 | const secretKey = await new HMAC("SHA-256").generateKey(); 216 | const jwt = await createJWT( 217 | "HS256", 218 | secretKey, 219 | {}, 220 | { 221 | notBefore: new Date(Date.now() + 1000) 222 | } 223 | ); 224 | await expect(validateJWT("HS512", secretKey, jwt)).rejects.toThrowError(); 225 | }); 226 | test("Throws on invalid signature", async () => { 227 | const secretKey = await new HMAC("SHA-256").generateKey(); 228 | const jwt = await createJWT( 229 | "HS256", 230 | secretKey, 231 | {}, 232 | { 233 | notBefore: new Date(Date.now() + 1000) 234 | } 235 | ); 236 | const invalidKey = await new HMAC("SHA-256").generateKey(); 237 | await expect(validateJWT("HS512", invalidKey, jwt)).rejects.toThrowError(); 238 | }); 239 | test("Throws on invalid JWT", async () => { 240 | const secretKey = await new HMAC("SHA-256").generateKey(); 241 | await expect(validateJWT("HS256", secretKey, "huhuihdeuihdiheud")).rejects.toThrowError(); 242 | await expect( 243 | validateJWT("HS256", secretKey, "huhuihdeuihdiheudheiuhdehd.dededed.deded") 244 | ).rejects.toThrowError(); 245 | }); 246 | }); 247 | 248 | const ecdsaDictionary = { 249 | ES256: { 250 | hash: "SHA-256", 251 | curve: "P-256" 252 | }, 253 | ES384: { 254 | hash: "SHA-384", 255 | curve: "P-384" 256 | }, 257 | ES512: { 258 | hash: "SHA-512", 259 | curve: "P-521" 260 | } 261 | } as const; 262 | 263 | const hmacDictionary = { 264 | HS256: "SHA-256", 265 | HS384: "SHA-384", 266 | HS512: "SHA-512" 267 | } as const; 268 | 269 | const rsassapkcs1v1_5Dictionary = { 270 | RS256: "SHA-256", 271 | RS384: "SHA-384", 272 | RS512: "SHA-512" 273 | } as const; 274 | 275 | const rsassapssDictionary = { 276 | PS256: "SHA-256", 277 | PS384: "SHA-384", 278 | PS512: "SHA-512" 279 | } as const; 280 | -------------------------------------------------------------------------------- /src/jwt/index.ts: -------------------------------------------------------------------------------- 1 | import { ECDSA, HMAC, RSASSAPKCS1v1_5, RSASSAPSS } from "../crypto/index.js"; 2 | import { base64url } from "../encoding/index.js"; 3 | import { isWithinExpirationDate } from "../index.js"; 4 | 5 | import type { TimeSpan, TypedArray } from "../index.js"; 6 | 7 | export type JWTAlgorithm = 8 | | "HS256" 9 | | "HS384" 10 | | "HS512" 11 | | "RS256" 12 | | "RS384" 13 | | "RS512" 14 | | "ES256" 15 | | "ES384" 16 | | "ES512" 17 | | "PS256" 18 | | "PS384" 19 | | "PS512"; 20 | 21 | export async function createJWT( 22 | algorithm: JWTAlgorithm, 23 | key: ArrayBuffer | TypedArray, 24 | payloadClaims: Record, 25 | options?: { 26 | headers?: Record; 27 | expiresIn?: TimeSpan; 28 | issuer?: string; 29 | subject?: string; 30 | audiences?: string[]; 31 | notBefore?: Date; 32 | includeIssuedTimestamp?: boolean; 33 | jwtId?: string; 34 | } 35 | ): Promise { 36 | const header: JWTHeader = { 37 | alg: algorithm, 38 | typ: "JWT", 39 | ...options?.headers 40 | }; 41 | const payload: JWTPayload = { 42 | ...payloadClaims 43 | }; 44 | if (options?.audiences !== undefined) { 45 | payload.aud = options.audiences; 46 | } 47 | if (options?.subject !== undefined) { 48 | payload.sub = options.subject; 49 | } 50 | if (options?.issuer !== undefined) { 51 | payload.iss = options.issuer; 52 | } 53 | if (options?.jwtId !== undefined) { 54 | payload.jti = options.jwtId; 55 | } 56 | if (options?.expiresIn !== undefined) { 57 | payload.exp = Math.floor(Date.now() / 1000) + options.expiresIn.seconds(); 58 | } 59 | if (options?.notBefore !== undefined) { 60 | payload.nbf = Math.floor(options.notBefore.getTime() / 1000); 61 | } 62 | if (options?.includeIssuedTimestamp === true) { 63 | payload.iat = Math.floor(Date.now() / 1000); 64 | } 65 | const textEncoder = new TextEncoder(); 66 | const headerPart = base64url.encode(textEncoder.encode(JSON.stringify(header)), { 67 | includePadding: false 68 | }); 69 | const payloadPart = base64url.encode(textEncoder.encode(JSON.stringify(payload)), { 70 | includePadding: false 71 | }); 72 | const data = textEncoder.encode([headerPart, payloadPart].join(".")); 73 | const signature = await getAlgorithm(algorithm).sign(key, data); 74 | const signaturePart = base64url.encode(new Uint8Array(signature), { 75 | includePadding: false 76 | }); 77 | const value = [headerPart, payloadPart, signaturePart].join("."); 78 | return value; 79 | } 80 | 81 | export async function validateJWT( 82 | algorithm: JWTAlgorithm, 83 | key: ArrayBuffer | TypedArray, 84 | jwt: string 85 | ): Promise { 86 | const parsedJWT = parseJWT(jwt); 87 | if (!parsedJWT) { 88 | throw new Error("Invalid JWT"); 89 | } 90 | if (parsedJWT.algorithm !== algorithm) { 91 | throw new Error("Invalid algorithm"); 92 | } 93 | if (parsedJWT.expiresAt && !isWithinExpirationDate(parsedJWT.expiresAt)) { 94 | throw new Error("Expired JWT"); 95 | } 96 | if (parsedJWT.notBefore && Date.now() < parsedJWT.notBefore.getTime()) { 97 | throw new Error("Inactive JWT"); 98 | } 99 | const signature = base64url.decode(parsedJWT.parts[2], { 100 | strict: false 101 | }); 102 | const data = new TextEncoder().encode(parsedJWT.parts[0] + "." + parsedJWT.parts[1]); 103 | const validSignature = await getAlgorithm(parsedJWT.algorithm).verify(key, signature, data); 104 | if (!validSignature) { 105 | throw new Error("Invalid signature"); 106 | } 107 | return parsedJWT; 108 | } 109 | 110 | function getJWTParts(jwt: string): [header: string, payload: string, signature: string] | null { 111 | const jwtParts = jwt.split("."); 112 | if (jwtParts.length !== 3) { 113 | return null; 114 | } 115 | return jwtParts as [string, string, string]; 116 | } 117 | 118 | export function parseJWT(jwt: string): JWT | null { 119 | const jwtParts = getJWTParts(jwt); 120 | if (!jwtParts) { 121 | return null; 122 | } 123 | const textDecoder = new TextDecoder(); 124 | const rawHeader = base64url.decode(jwtParts[0], { 125 | strict: false 126 | }); 127 | const rawPayload = base64url.decode(jwtParts[1], { 128 | strict: false 129 | }); 130 | const header: unknown = JSON.parse(textDecoder.decode(rawHeader)); 131 | if (typeof header !== "object" || header === null) { 132 | return null; 133 | } 134 | if (!("alg" in header) || !isValidAlgorithm(header.alg)) { 135 | return null; 136 | } 137 | if ("typ" in header && header.typ !== "JWT") { 138 | return null; 139 | } 140 | const payload: unknown = JSON.parse(textDecoder.decode(rawPayload)); 141 | if (typeof payload !== "object" || payload === null) { 142 | return null; 143 | } 144 | const properties: JWTProperties = { 145 | algorithm: header.alg, 146 | expiresAt: null, 147 | subject: null, 148 | issuedAt: null, 149 | issuer: null, 150 | jwtId: null, 151 | audiences: null, 152 | notBefore: null 153 | }; 154 | if ("exp" in payload) { 155 | if (typeof payload.exp !== "number") { 156 | return null; 157 | } 158 | properties.expiresAt = new Date(payload.exp * 1000); 159 | } 160 | if ("iss" in payload) { 161 | if (typeof payload.iss !== "string") { 162 | return null; 163 | } 164 | properties.issuer = payload.iss; 165 | } 166 | if ("sub" in payload) { 167 | if (typeof payload.sub !== "string") { 168 | return null; 169 | } 170 | properties.subject = payload.sub; 171 | } 172 | if ("aud" in payload) { 173 | if (!Array.isArray(payload.aud)) { 174 | if (typeof payload.aud !== "string") { 175 | return null; 176 | } 177 | properties.audiences = [payload.aud]; 178 | } else { 179 | for (const item of payload.aud) { 180 | if (typeof item !== "string") { 181 | return null; 182 | } 183 | } 184 | properties.audiences = payload.aud; 185 | } 186 | } 187 | if ("nbf" in payload) { 188 | if (typeof payload.nbf !== "number") { 189 | return null; 190 | } 191 | properties.notBefore = new Date(payload.nbf * 1000); 192 | } 193 | if ("iat" in payload) { 194 | if (typeof payload.iat !== "number") { 195 | return null; 196 | } 197 | properties.issuedAt = new Date(payload.iat * 1000); 198 | } 199 | if ("jti" in payload) { 200 | if (typeof payload.jti !== "string") { 201 | return null; 202 | } 203 | properties.jwtId = payload.jti; 204 | } 205 | return { 206 | value: jwt, 207 | header: { 208 | ...header, 209 | typ: "JWT", 210 | alg: header.alg 211 | }, 212 | payload: { 213 | ...payload 214 | }, 215 | parts: jwtParts, 216 | ...properties 217 | }; 218 | } 219 | 220 | interface JWTProperties { 221 | algorithm: JWTAlgorithm; 222 | expiresAt: Date | null; 223 | issuer: string | null; 224 | subject: string | null; 225 | audiences: string[] | null; 226 | notBefore: Date | null; 227 | issuedAt: Date | null; 228 | jwtId: string | null; 229 | } 230 | 231 | export interface JWT extends JWTProperties { 232 | value: string; 233 | header: object; 234 | payload: object; 235 | parts: [header: string, payload: string, signature: string]; 236 | } 237 | 238 | function getAlgorithm(algorithm: JWTAlgorithm): ECDSA | HMAC | RSASSAPKCS1v1_5 | RSASSAPSS { 239 | if (algorithm === "ES256" || algorithm === "ES384" || algorithm === "ES512") { 240 | return new ECDSA(ecdsaDictionary[algorithm].hash, ecdsaDictionary[algorithm].curve); 241 | } 242 | if (algorithm === "HS256" || algorithm === "HS384" || algorithm === "HS512") { 243 | return new HMAC(hmacDictionary[algorithm]); 244 | } 245 | if (algorithm === "RS256" || algorithm === "RS384" || algorithm === "RS512") { 246 | return new RSASSAPKCS1v1_5(rsassapkcs1v1_5Dictionary[algorithm]); 247 | } 248 | if (algorithm === "PS256" || algorithm === "PS384" || algorithm === "PS512") { 249 | return new RSASSAPSS(rsassapssDictionary[algorithm]); 250 | } 251 | throw new TypeError("Invalid algorithm"); 252 | } 253 | 254 | function isValidAlgorithm(maybeValidAlgorithm: unknown): maybeValidAlgorithm is JWTAlgorithm { 255 | if (typeof maybeValidAlgorithm !== "string") return false; 256 | return [ 257 | "HS256", 258 | "HS384", 259 | "HS512", 260 | "RS256", 261 | "RS384", 262 | "RS512", 263 | "ES256", 264 | "ES384", 265 | "ES512", 266 | "PS256", 267 | "PS384", 268 | "PS512" 269 | ].includes(maybeValidAlgorithm); 270 | } 271 | 272 | interface JWTHeader { 273 | typ: "JWT"; 274 | alg: JWTAlgorithm; 275 | [header: string]: any; 276 | } 277 | 278 | interface JWTPayload { 279 | exp?: number; 280 | iss?: string; 281 | aud?: string[] | string; 282 | jti?: string; 283 | nbf?: number; 284 | sub?: string; 285 | iat?: number; 286 | [claim: string]: any; 287 | } 288 | 289 | const ecdsaDictionary = { 290 | ES256: { 291 | hash: "SHA-256", 292 | curve: "P-256" 293 | }, 294 | ES384: { 295 | hash: "SHA-384", 296 | curve: "P-384" 297 | }, 298 | ES512: { 299 | hash: "SHA-512", 300 | curve: "P-521" 301 | } 302 | } as const; 303 | 304 | const hmacDictionary = { 305 | HS256: "SHA-256", 306 | HS384: "SHA-384", 307 | HS512: "SHA-512" 308 | } as const; 309 | 310 | const rsassapkcs1v1_5Dictionary = { 311 | RS256: "SHA-256", 312 | RS384: "SHA-384", 313 | RS512: "SHA-512" 314 | } as const; 315 | 316 | const rsassapssDictionary = { 317 | PS256: "SHA-256", 318 | PS384: "SHA-384", 319 | PS512: "SHA-512" 320 | } as const; 321 | -------------------------------------------------------------------------------- /src/oauth2/index.ts: -------------------------------------------------------------------------------- 1 | import { sha256 } from "../crypto/index.js"; 2 | import { base64, base64url } from "../encoding/index.js"; 3 | 4 | export class OAuth2Client { 5 | public clientId: string; 6 | 7 | private authorizeEndpoint: string; 8 | private tokenEndpoint: string; 9 | private redirectURI: string | null; 10 | 11 | constructor( 12 | clientId: string, 13 | authorizeEndpoint: string, 14 | tokenEndpoint: string, 15 | options?: { 16 | redirectURI?: string; 17 | } 18 | ) { 19 | this.clientId = clientId; 20 | this.authorizeEndpoint = authorizeEndpoint; 21 | this.tokenEndpoint = tokenEndpoint; 22 | this.redirectURI = options?.redirectURI ?? null; 23 | } 24 | 25 | public async createAuthorizationURL(options?: { 26 | state?: string; 27 | codeVerifier?: string; 28 | codeChallengeMethod?: "S256" | "plain"; 29 | scopes?: string[]; 30 | }): Promise { 31 | const scopes = Array.from(new Set(options?.scopes ?? [])); // remove duplicates 32 | const authorizationUrl = new URL(this.authorizeEndpoint); 33 | authorizationUrl.searchParams.set("response_type", "code"); 34 | authorizationUrl.searchParams.set("client_id", this.clientId); 35 | if (options?.state !== undefined) { 36 | authorizationUrl.searchParams.set("state", options.state); 37 | } 38 | if (scopes.length > 0) { 39 | authorizationUrl.searchParams.set("scope", scopes.join(" ")); 40 | } 41 | if (this.redirectURI !== null) { 42 | authorizationUrl.searchParams.set("redirect_uri", this.redirectURI); 43 | } 44 | if (options?.codeVerifier !== undefined) { 45 | const codeChallengeMethod = options?.codeChallengeMethod ?? "S256"; 46 | if (codeChallengeMethod === "S256") { 47 | const codeChallengeBuffer = await sha256(new TextEncoder().encode(options.codeVerifier)); 48 | const codeChallenge = base64url.encode(new Uint8Array(codeChallengeBuffer), { 49 | includePadding: false 50 | }); 51 | authorizationUrl.searchParams.set("code_challenge", codeChallenge); 52 | authorizationUrl.searchParams.set("code_challenge_method", "S256"); 53 | } else if (codeChallengeMethod === "plain") { 54 | authorizationUrl.searchParams.set("code_challenge", options.codeVerifier); 55 | authorizationUrl.searchParams.set("code_challenge_method", "plain"); 56 | } else { 57 | throw new TypeError(`Invalid value for 'codeChallengeMethod': ${codeChallengeMethod}`); 58 | } 59 | } 60 | return authorizationUrl; 61 | } 62 | 63 | public async validateAuthorizationCode<_TokenResponseBody extends TokenResponseBody>( 64 | authorizationCode: string, 65 | options?: { 66 | codeVerifier?: string; 67 | credentials?: string; 68 | authenticateWith?: "http_basic_auth" | "request_body"; 69 | } 70 | ): Promise<_TokenResponseBody> { 71 | const body = new URLSearchParams(); 72 | body.set("code", authorizationCode); 73 | body.set("client_id", this.clientId); 74 | body.set("grant_type", "authorization_code"); 75 | 76 | if (this.redirectURI !== null) { 77 | body.set("redirect_uri", this.redirectURI); 78 | } 79 | if (options?.codeVerifier !== undefined) { 80 | body.set("code_verifier", options.codeVerifier); 81 | } 82 | return await this.sendTokenRequest<_TokenResponseBody>(body, options); 83 | } 84 | 85 | public async refreshAccessToken<_TokenResponseBody extends TokenResponseBody>( 86 | refreshToken: string, 87 | options?: { 88 | credentials?: string; 89 | authenticateWith?: "http_basic_auth" | "request_body"; 90 | scopes?: string[]; 91 | } 92 | ): Promise<_TokenResponseBody> { 93 | const body = new URLSearchParams(); 94 | body.set("refresh_token", refreshToken); 95 | body.set("client_id", this.clientId); 96 | body.set("grant_type", "refresh_token"); 97 | 98 | const scopes = Array.from(new Set(options?.scopes ?? [])); // remove duplicates 99 | if (scopes.length > 0) { 100 | body.set("scope", scopes.join(" ")); 101 | } 102 | 103 | return await this.sendTokenRequest<_TokenResponseBody>(body, options); 104 | } 105 | 106 | private async sendTokenRequest<_TokenResponseBody extends TokenResponseBody>( 107 | body: URLSearchParams, 108 | options?: { 109 | credentials?: string; 110 | authenticateWith?: "http_basic_auth" | "request_body"; 111 | } 112 | ): Promise<_TokenResponseBody> { 113 | const headers = new Headers(); 114 | headers.set("Content-Type", "application/x-www-form-urlencoded"); 115 | headers.set("Accept", "application/json"); 116 | headers.set("User-Agent", "oslo"); 117 | 118 | if (options?.credentials !== undefined) { 119 | const authenticateWith = options?.authenticateWith ?? "http_basic_auth"; 120 | if (authenticateWith === "http_basic_auth") { 121 | const encodedCredentials = base64.encode( 122 | new TextEncoder().encode(`${this.clientId}:${options.credentials}`) 123 | ); 124 | headers.set("Authorization", `Basic ${encodedCredentials}`); 125 | } else if (authenticateWith === "request_body") { 126 | body.set("client_secret", options.credentials); 127 | } else { 128 | throw new TypeError(`Invalid value for 'authenticateWith': ${authenticateWith}`); 129 | } 130 | } 131 | 132 | const request = new Request(this.tokenEndpoint, { 133 | method: "POST", 134 | headers, 135 | body 136 | }); 137 | const response = await fetch(request); 138 | const result: _TokenResponseBody | TokenErrorResponseBody = await response.json(); 139 | 140 | // providers are allowed to return non-400 status code for errors 141 | if (!("access_token" in result) && "error" in result) { 142 | throw new OAuth2RequestError(request, result); 143 | } else if (!response.ok) { 144 | throw new OAuth2RequestError(request, {}); 145 | } 146 | return result; 147 | } 148 | } 149 | 150 | export function generateCodeVerifier(): string { 151 | const randomValues = new Uint8Array(32); 152 | crypto.getRandomValues(randomValues); 153 | return base64url.encode(randomValues, { 154 | includePadding: false 155 | }); 156 | } 157 | 158 | export function generateState(): string { 159 | const randomValues = new Uint8Array(32); 160 | crypto.getRandomValues(randomValues); 161 | return base64url.encode(randomValues, { 162 | includePadding: false 163 | }); 164 | } 165 | 166 | export class OAuth2RequestError extends Error { 167 | public request: Request; 168 | public description: string | null; 169 | constructor(request: Request, body: Partial) { 170 | super(body.error ?? ""); 171 | this.request = request; 172 | this.description = body.error_description ?? null; 173 | } 174 | } 175 | 176 | interface TokenErrorResponseBody { 177 | error: string; 178 | error_description?: string; 179 | } 180 | 181 | export interface TokenResponseBody { 182 | access_token: string; 183 | token_type?: string; 184 | expires_in?: number; 185 | refresh_token?: string; 186 | scope?: string; 187 | } 188 | -------------------------------------------------------------------------------- /src/otp/hotp.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "vitest"; 2 | import { test } from "vitest"; 3 | import { generateHOTP } from "./hotp.js"; 4 | 5 | test("generateHOTP()", async () => { 6 | const secret = new Uint8Array([ 7 | 0x63, 0x07, 0x87, 0x06, 0xe4, 0x89, 0x1b, 0x07, 0x85, 0xba, 0x42, 0xbd, 0x23, 0xac, 0xdd, 0x09, 8 | 0xe4, 0x69, 0x33, 0x63, 0xbe, 0xfa, 0x25, 0xa4, 0x13, 0x46, 0xee, 0x0b, 0xda, 0xb0, 0x72, 0x4c, 9 | 0xa0, 0x8f, 0x8d, 0x26, 0x63, 0x0e, 0xb5, 0x6c, 0xa3, 0xfd, 0xce, 0x6c, 0xc0, 0x0e, 0xf8, 0x65, 10 | 0x6d, 0x1f, 0xeb, 0xc7, 0x35, 0x92, 0x87, 0x16, 0x3d, 0x11, 0x34, 0x20, 0x00, 0x7a, 0x18, 0x1c 11 | ]); 12 | expect(generateHOTP(secret, 0)).resolves.toBe("173573"); 13 | expect(generateHOTP(secret, 10)).resolves.toBe("110880"); 14 | expect(generateHOTP(secret, 100)).resolves.toBe("020803"); 15 | expect(generateHOTP(secret, 1000)).resolves.toBe("115716"); 16 | }); 17 | -------------------------------------------------------------------------------- /src/otp/hotp.ts: -------------------------------------------------------------------------------- 1 | import { binaryToInteger, byteToBinary, bytesToBinary } from "../bytes.js"; 2 | import { HMAC } from "../crypto/hmac.js"; 3 | 4 | import type { TypedArray } from "../index.js"; 5 | 6 | export async function generateHOTP( 7 | key: ArrayBuffer | TypedArray, 8 | counter: number, 9 | digits: number = 6 10 | ): Promise { 11 | if (digits > 8) { 12 | throw new TypeError("Digits must be 8 or smaller"); 13 | } 14 | const counterBytes = intTo8Bytes(counter); 15 | const HS = await new HMAC("SHA-1").sign(key, counterBytes); 16 | const SBites = truncate(new Uint8Array(HS)); 17 | const SNum = binaryToInteger(SBites); 18 | const D = SNum % 10 ** digits; 19 | return D.toString().padStart(digits, "0"); 20 | } 21 | 22 | function truncate(data: Uint8Array): string { 23 | const offset = binaryToInteger(byteToBinary(data[data.byteLength - 1]!).slice(4)); 24 | return bytesToBinary(data).slice(offset * 8 + 1, (offset + 4) * 8); 25 | } 26 | 27 | function intTo8Bytes(int: number): Uint8Array { 28 | const result = new Uint8Array(8); 29 | const bits = int.toString(2).padStart(8 * 8, "0"); 30 | for (let i = 0; i < 8; i++) { 31 | result[i] = binaryToInteger(bits.slice(i * 8, (i + 1) * 8)); 32 | } 33 | return result; 34 | } 35 | -------------------------------------------------------------------------------- /src/otp/index.ts: -------------------------------------------------------------------------------- 1 | export { generateHOTP } from "./hotp.js"; 2 | export { TOTPController } from "./totp.js"; 3 | export { createHOTPKeyURI, createTOTPKeyURI } from "./uri.js"; 4 | -------------------------------------------------------------------------------- /src/otp/totp.ts: -------------------------------------------------------------------------------- 1 | import { TimeSpan } from "../index.js"; 2 | import { generateHOTP } from "./hotp.js"; 3 | 4 | import type { TypedArray } from "../index.js"; 5 | 6 | export class TOTPController { 7 | private digits: number; 8 | private period: TimeSpan; 9 | 10 | constructor(options?: { digits?: number; period?: TimeSpan }) { 11 | this.digits = options?.digits ?? 6; 12 | this.period = options?.period ?? new TimeSpan(30, "s"); 13 | } 14 | 15 | public async generate(secret: ArrayBuffer | TypedArray): Promise { 16 | const counter = Math.floor(Date.now() / this.period.milliseconds()); 17 | return await generateHOTP(secret, counter, this.digits); 18 | } 19 | 20 | public async verify(totp: string, secret: ArrayBuffer | TypedArray): Promise { 21 | const expectedTOTP = await this.generate(secret); 22 | return totp === expectedTOTP; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/otp/uri.ts: -------------------------------------------------------------------------------- 1 | import { base32 } from "../encoding/index.js"; 2 | 3 | import type { TimeSpan, TypedArray } from "../index.js"; 4 | 5 | export function createTOTPKeyURI( 6 | issuer: string, 7 | accountName: string, 8 | secret: ArrayBuffer | TypedArray, 9 | options?: { 10 | digits?: number; 11 | period?: TimeSpan; 12 | } 13 | ): string { 14 | const [baseURI, params] = createKeyURIBase("totp", issuer, accountName, secret, options); 15 | if (options?.period !== undefined) { 16 | params.set("period", options.period.seconds().toString()); 17 | } 18 | return baseURI + "?" + params.toString(); 19 | } 20 | 21 | export function createHOTPKeyURI( 22 | issuer: string, 23 | accountName: string, 24 | secret: ArrayBuffer | TypedArray, 25 | options?: { 26 | counter?: number; 27 | digits?: number; 28 | } 29 | ): string { 30 | const [baseURI, params] = createKeyURIBase("hotp", issuer, accountName, secret, options); 31 | const counter = options?.counter ?? 0; 32 | params.set("counter", counter.toString()); 33 | return baseURI + "?" + params.toString(); 34 | } 35 | 36 | function createKeyURIBase( 37 | type: "totp" | "hotp", 38 | issuer: string, 39 | accountName: string, 40 | secret: ArrayBuffer | TypedArray, 41 | options?: { 42 | digits?: number; 43 | } 44 | ): [baseURI: string, params: URLSearchParams] { 45 | const encodedIssuer = encodeURIComponent(issuer); 46 | const encodedAccountName = encodeURIComponent(accountName); 47 | const baseURI = `otpauth://${type}/${encodedIssuer}:${encodedAccountName}`; 48 | const params = new URLSearchParams({ 49 | secret: base32.encode(new Uint8Array(secret), { 50 | includePadding: false 51 | }), 52 | issuer 53 | }); 54 | if (options?.digits !== undefined) { 55 | params.set("digits", options.digits.toString()); 56 | } 57 | return [baseURI, params]; 58 | } 59 | -------------------------------------------------------------------------------- /src/password/argon2id.ts: -------------------------------------------------------------------------------- 1 | import { hash, verify } from "@node-rs/argon2"; 2 | 3 | import type { PasswordHashingAlgorithm } from "./index.js"; 4 | import type { TypedArray } from "../index.js"; 5 | 6 | const v0x13 = 1; 7 | 8 | export class Argon2id implements PasswordHashingAlgorithm { 9 | constructor(options?: { 10 | memorySize?: number; 11 | iterations?: number; 12 | tagLength?: number; 13 | parallelism?: number; 14 | secret?: ArrayBuffer | TypedArray; 15 | }) { 16 | this.memorySize = options?.memorySize ?? 19456; 17 | this.iterations = options?.iterations ?? 2; 18 | this.tagLength = options?.tagLength ?? 32; 19 | this.parallelism = options?.parallelism ?? 1; 20 | this.secret = options?.secret ?? null; 21 | } 22 | 23 | private memorySize?: number; 24 | private iterations?: number; 25 | private tagLength?: number; 26 | private parallelism?: number; 27 | private secret: ArrayBuffer | TypedArray | null; 28 | 29 | public async hash(password: string): Promise { 30 | return await hash(password.normalize("NFKC"), { 31 | memoryCost: this.memorySize, 32 | timeCost: this.iterations, 33 | outputLen: this.tagLength, 34 | parallelism: this.parallelism, 35 | version: v0x13, 36 | secret: this.secret ? Buffer.from(this.secret) : undefined 37 | }); 38 | } 39 | 40 | public async verify(hash: string, password: string): Promise { 41 | return await verify(hash, password.normalize("NFKC"), { 42 | memoryCost: this.memorySize, 43 | timeCost: this.iterations, 44 | outputLen: this.tagLength, 45 | parallelism: this.parallelism, 46 | version: v0x13, 47 | secret: this.secret ? Buffer.from(this.secret) : undefined 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/password/bcrypt.ts: -------------------------------------------------------------------------------- 1 | import { hash, verify } from "@node-rs/bcrypt"; 2 | 3 | import type { PasswordHashingAlgorithm } from "./index.js"; 4 | 5 | export class Bcrypt implements PasswordHashingAlgorithm { 6 | constructor(options?: { cost?: number }) { 7 | this.cost = options?.cost ?? 10; 8 | } 9 | 10 | private cost: number; 11 | 12 | public async hash(password: string): Promise { 13 | return await hash(password.normalize("NFKC"), this.cost); 14 | } 15 | 16 | public async verify(hash: string, password: string): Promise { 17 | return await verify(password.normalize("NFKC"), hash); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/password/index.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | import { Argon2id, Bcrypt, Scrypt } from "./index.js"; 3 | import { encodeHex } from "../encoding/index.js"; 4 | 5 | test("Argon2id", async () => { 6 | const password = encodeHex(crypto.getRandomValues(new Uint8Array(32))); 7 | const argon2id = new Argon2id(); 8 | const hash = await argon2id.hash(password); 9 | expect(argon2id.verify(hash, password)).resolves.toBe(true); 10 | }); 11 | 12 | test("Bcrypt", async () => { 13 | const password = encodeHex(crypto.getRandomValues(new Uint8Array(32))); 14 | const bcrypt = new Bcrypt(); 15 | const hash = await bcrypt.hash(password); 16 | expect(bcrypt.verify(hash, password)).resolves.toBe(true); 17 | }); 18 | 19 | test("Argon2id", async () => { 20 | const password = encodeHex(crypto.getRandomValues(new Uint8Array(32))); 21 | const scrypt = new Scrypt(); 22 | const hash = await scrypt.hash(password); 23 | expect(scrypt.verify(hash, password)).resolves.toBe(true); 24 | }); 25 | -------------------------------------------------------------------------------- /src/password/index.ts: -------------------------------------------------------------------------------- 1 | export interface PasswordHashingAlgorithm { 2 | hash(password: string): Promise; 3 | verify(hash: string, password: string): Promise; 4 | } 5 | 6 | export { Argon2id } from "./argon2id.js"; 7 | 8 | export { Scrypt } from "./scrypt.js"; 9 | 10 | export { Bcrypt } from "./bcrypt.js"; 11 | -------------------------------------------------------------------------------- /src/password/scrypt.ts: -------------------------------------------------------------------------------- 1 | import { scrypt } from "node:crypto"; 2 | import { decodeHex, encodeHex } from "../encoding/index.js"; 3 | import { constantTimeEqual } from "../crypto/index.js"; 4 | 5 | import type { PasswordHashingAlgorithm } from "./index.js"; 6 | 7 | export class Scrypt implements PasswordHashingAlgorithm { 8 | constructor(options?: { N?: number; r?: number; p?: number; dkLen?: number }) { 9 | this.N = options?.N ?? 16384; 10 | this.r = options?.r ?? 16; 11 | this.p = options?.p ?? 1; 12 | this.dkLen = options?.dkLen ?? 64; 13 | } 14 | 15 | private N: number; 16 | private r: number; 17 | private p: number; 18 | private dkLen: number; 19 | 20 | public async hash(password: string): Promise { 21 | const salt = encodeHex(crypto.getRandomValues(new Uint8Array(16))); 22 | const key = await this.generateKey(password, salt); 23 | return `${salt}:${encodeHex(key)}`; 24 | } 25 | 26 | public async verify(hash: string, password: string): Promise { 27 | const [salt, key] = hash.split(":"); 28 | const targetKey = await this.generateKey(password, salt!); 29 | return constantTimeEqual(targetKey, decodeHex(key!)); 30 | } 31 | 32 | private async generateKey(password: string, salt: string): Promise { 33 | return await new Promise((resolve, reject) => { 34 | scrypt( 35 | password.normalize("NFKC"), 36 | salt!, 37 | this.dkLen, 38 | { 39 | N: this.N, 40 | p: this.p, 41 | r: this.r, 42 | // errors when 128 * N * r > `maxmem` (approximately) 43 | maxmem: 128 * this.N * this.r * 2 44 | }, 45 | (err, buff) => { 46 | if (err) return reject(err); 47 | return resolve(buff); 48 | } 49 | ); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/request/index.test.ts: -------------------------------------------------------------------------------- 1 | import { verifyRequestOrigin } from "./index.js"; 2 | import { describe, test, expect } from "vitest"; 3 | 4 | describe("verifyRequestOrigin()", () => { 5 | test("checks if origin and host matches", () => { 6 | expect(verifyRequestOrigin("https://example.com", ["example.com"])).toBe(true); 7 | expect(verifyRequestOrigin("https://example.co.jp", ["example.co.jp"])).toBe(true); 8 | expect(verifyRequestOrigin("https://example.com", ["invalid.com"])).toBe(false); 9 | }); 10 | 11 | test("allows full urls for host", () => { 12 | expect(verifyRequestOrigin("https://example.com", ["https://example.com"])).toBe(true); 13 | expect(verifyRequestOrigin("https://example.co.jp", ["https://example.co.jp"])).toBe(true); 14 | expect(verifyRequestOrigin("https://example.com", ["https://invalid.com"])).toBe(false); 15 | }); 16 | 17 | test("checks port", () => { 18 | expect(verifyRequestOrigin("https://example.com:1000", ["example.com:1000"])).toBe(true); 19 | expect(verifyRequestOrigin("https://example.com:1000", ["example.com:2000"])).toBe(false); 20 | }); 21 | 22 | test("IDN", () => { 23 | expect(verifyRequestOrigin("http://xn--zckzah.com", ["xn--zckzah.com"])).toBe(true); 24 | expect(verifyRequestOrigin("http://xn--zckzah.com", ["テスト.com"])).toBe(true); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/request/index.ts: -------------------------------------------------------------------------------- 1 | export function verifyRequestOrigin(origin: string, allowedDomains: string[]): boolean { 2 | if (!origin || allowedDomains.length === 0) return false; 3 | const originHost = safeURL(origin)?.host ?? null; 4 | if (!originHost) return false; 5 | for (const domain of allowedDomains) { 6 | let host: string | null; 7 | if (domain.startsWith("http://") || domain.startsWith("https://")) { 8 | host = safeURL(domain)?.host ?? null; 9 | } else { 10 | host = safeURL("https://" + domain)?.host ?? null; 11 | } 12 | if (originHost === host) return true; 13 | } 14 | return false; 15 | } 16 | 17 | function safeURL(url: URL | string): URL | null { 18 | try { 19 | return new URL(url); 20 | } catch { 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/webauthn/index.ts: -------------------------------------------------------------------------------- 1 | import { base64url } from "../encoding/index.js"; 2 | import { compareBytes } from "../bytes.js"; 3 | import { ECDSA, RSASSAPKCS1v1_5 } from "../crypto/index.js"; 4 | 5 | import type { TypedArray } from "../index.js"; 6 | 7 | export interface AttestationResponse { 8 | clientDataJSON: ArrayBuffer | TypedArray; 9 | authenticatorData: ArrayBuffer | TypedArray; 10 | } 11 | 12 | export interface AssertionResponse { 13 | clientDataJSON: ArrayBuffer | TypedArray; 14 | authenticatorData: ArrayBuffer | TypedArray; 15 | signature: ArrayBuffer | TypedArray; 16 | } 17 | 18 | export class WebAuthnController { 19 | private originURL: URL; 20 | constructor(origin: string) { 21 | this.originURL = new URL(origin); 22 | } 23 | 24 | public async validateAttestationResponse( 25 | response: AttestationResponse, 26 | challenge: ArrayBuffer | TypedArray 27 | ): Promise { 28 | const validClientDataJSON = this.verifyClientDataJSON( 29 | "webauthn.create", 30 | response.clientDataJSON, 31 | challenge 32 | ); 33 | if (!validClientDataJSON) { 34 | throw new Error("Failed to validate client data JSON"); 35 | } 36 | 37 | const validAuthenticatorData = await this.verifyAuthenticatorData(response.authenticatorData); 38 | if (!validAuthenticatorData) { 39 | throw new Error("Failed to validate authenticator data"); 40 | } 41 | } 42 | 43 | public async validateAssertionResponse( 44 | algorithm: "ES256" | "RS256", 45 | publicKey: ArrayBuffer | TypedArray, 46 | response: AssertionResponse, 47 | challenge: ArrayBuffer | TypedArray 48 | ): Promise { 49 | const validClientDataJSON = this.verifyClientDataJSON( 50 | "webauthn.get", 51 | response.clientDataJSON, 52 | challenge 53 | ); 54 | if (!validClientDataJSON) { 55 | throw new Error("Failed to validate client data JSON"); 56 | } 57 | 58 | const validAuthenticatorData = await this.verifyAuthenticatorData(response.authenticatorData); 59 | if (!validAuthenticatorData) { 60 | throw new Error("Failed to validate authenticator data"); 61 | } 62 | 63 | if (algorithm === "ES256") { 64 | const signature = convertDERSignatureToECDSASignature(response.signature); 65 | const hash = await crypto.subtle.digest("SHA-256", response.clientDataJSON); 66 | const data = concatenateBuffer(response.authenticatorData, hash); 67 | const es256 = new ECDSA("SHA-256", "P-256"); 68 | const validSignature = await es256.verify(publicKey, signature, data); 69 | if (!validSignature) { 70 | throw new Error("Failed to validate signature"); 71 | } 72 | } else if (algorithm === "RS256") { 73 | const signature = convertDERSignatureToECDSASignature(response.signature); 74 | const hash = await crypto.subtle.digest("SHA-256", response.clientDataJSON); 75 | const data = concatenateBuffer(response.authenticatorData, hash); 76 | const rs256 = new RSASSAPKCS1v1_5("SHA-256"); 77 | const validSignature = await rs256.verify(publicKey, signature, data); 78 | if (!validSignature) { 79 | throw new Error("Failed to validate signature"); 80 | } 81 | } else { 82 | throw new TypeError(`Unknown algorithm: ${algorithm}`); 83 | } 84 | } 85 | 86 | private verifyClientDataJSON( 87 | type: "webauthn.create" | "webauthn.get", 88 | clientDataJSON: ArrayBuffer | TypedArray, 89 | challenge: ArrayBuffer | TypedArray 90 | ): boolean { 91 | const clientData: unknown = JSON.parse(new TextDecoder().decode(clientDataJSON)); 92 | if (!clientData || typeof clientData !== "object") { 93 | return false; 94 | } 95 | if (!("type" in clientData) || clientData.type !== type) { 96 | return false; 97 | } 98 | if (!("challenge" in clientData) || typeof clientData.challenge !== "string") { 99 | return false; 100 | } 101 | const clientDataChallengeBuffer = base64url.decode(clientData.challenge, { 102 | strict: false 103 | }); 104 | if (!compareBytes(clientDataChallengeBuffer, challenge)) { 105 | return false; 106 | } 107 | if (!("origin" in clientData) || clientData.origin !== this.originURL.origin) { 108 | return false; 109 | } 110 | return true; 111 | } 112 | 113 | private async verifyAuthenticatorData(authenticatorData: ArrayBuffer): Promise { 114 | const authData = new Uint8Array(authenticatorData); 115 | if (authData.byteLength < 37) { 116 | return false; 117 | } 118 | const rpIdHash = authData.slice(0, 32); 119 | const rpIdData = new TextEncoder().encode(this.originURL.hostname); 120 | const expectedRpIdHash = await crypto.subtle.digest("SHA-256", rpIdData); 121 | // compare buffer 122 | if (!compareBytes(rpIdHash, expectedRpIdHash)) { 123 | return false; 124 | } 125 | const flagsBits = authData[32]!.toString(2); 126 | if (flagsBits.charAt(flagsBits.length - 1) !== "1") { 127 | return false; 128 | } 129 | return true; 130 | } 131 | } 132 | 133 | function convertDERSignatureToECDSASignature(DERSignature: ArrayBuffer): ArrayBuffer { 134 | const signatureBytes = new Uint8Array(DERSignature); 135 | 136 | const rStart = 4; 137 | const rLength = signatureBytes[3]; 138 | const rEnd = rStart + rLength!; 139 | const DEREncodedR = signatureBytes.slice(rStart, rEnd); 140 | // DER encoded 32 bytes integers can have leading 0x00s or be smaller than 32 bytes 141 | const r = decodeDERInteger(DEREncodedR, 32); 142 | 143 | const sStart = rEnd + 2; 144 | const sEnd = signatureBytes.byteLength; 145 | const DEREncodedS = signatureBytes.slice(sStart, sEnd); 146 | // repeat the process 147 | const s = decodeDERInteger(DEREncodedS, 32); 148 | 149 | const ECDSASignature = new Uint8Array([...r, ...s]); 150 | return ECDSASignature.buffer; 151 | } 152 | 153 | function decodeDERInteger(integerBytes: Uint8Array, expectedLength: number): Uint8Array { 154 | if (integerBytes.byteLength === expectedLength) return integerBytes; 155 | if (integerBytes.byteLength < expectedLength) { 156 | return concatenateUint8Array( 157 | // add leading 0x00s if smaller than expected length 158 | new Uint8Array(expectedLength - integerBytes.byteLength).fill(0), 159 | integerBytes 160 | ); 161 | } 162 | // remove leading 0x00s if larger then expected length 163 | return integerBytes.slice(-32); 164 | } 165 | 166 | function concatenateBuffer( 167 | buffer1: ArrayBuffer | TypedArray, 168 | buffer2: ArrayBuffer | TypedArray 169 | ): ArrayBuffer { 170 | return concatenateUint8Array(new Uint8Array(buffer1), new Uint8Array(buffer2)).buffer; 171 | } 172 | 173 | function concatenateUint8Array(bytes1: Uint8Array, bytes2: Uint8Array): Uint8Array { 174 | const result = new Uint8Array(bytes1.byteLength + bytes2.byteLength); 175 | result.set(new Uint8Array(bytes1), 0); 176 | result.set(new Uint8Array(bytes2), bytes1.byteLength); 177 | return result; 178 | } 179 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declaration": true 6 | }, 7 | "exclude": ["src/**/*.test.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "es2022", 7 | "verbatimModuleSyntax": true, 8 | "allowJs": true, 9 | "resolveJsonModule": true, 10 | "moduleDetection": "force", 11 | "strict": true, 12 | "noUncheckedIndexedAccess": true, 13 | "moduleResolution": "NodeNext", 14 | "module": "NodeNext" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | setupFiles: ["vitest.setup.ts"] 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /vitest.setup.ts: -------------------------------------------------------------------------------- 1 | // making sure Date.now() returns the same time for easier testing 2 | 3 | const now = new Date(); 4 | 5 | class StaticDate extends Date { 6 | constructor(value?: any) { 7 | if (value === undefined) { 8 | super(now); 9 | } else { 10 | super(value); 11 | } 12 | } 13 | } 14 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 15 | // @ts-ignore 16 | globalThis.Date = StaticDate; 17 | 18 | globalThis.Date.now = (): number => now.getTime(); 19 | --------------------------------------------------------------------------------