├── .eslintrc
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── question.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── test.yaml
├── .gitignore
├── .prettierrc.js
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── config
├── tsconfig.base.json
├── tsconfig.lib.json
└── tsconfig.test.json
├── package.json
├── scripts
├── build.sh
├── start.sh
└── test.sh
├── src
├── index.ts
├── strategy.ts
└── types.ts
├── test
├── lib
│ ├── constants.ts
│ └── factories.ts
├── spec
│ └── strategy
│ │ ├── authenticate.spec.ts
│ │ └── constructor.spec.ts
└── tsconfig.json
├── tsconfig.json
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@ikscodes/eslint-config"],
3 | "parserOptions": {
4 | "project": [
5 | "./tsconfig.json",
6 | "./test/tsconfig.json"
7 | ]
8 | },
9 | "rules": {
10 | "import/extensions": 0,
11 | "no-alert": 0,
12 | "@typescript-eslint/await-thenable": 0,
13 | "react/button-has-type": 0,
14 | "no-cond-assign": 0,
15 | "class-methods-use-this": 0,
16 | "no-underscore-dangle": 0,
17 | "no-useless-constructor": 0,
18 | "consistent-return": 0,
19 | "@typescript-eslint/no-empty-function": 0
20 | },
21 | "settings": {
22 | "import/resolver": {
23 | "typescript": {
24 | "directory": [
25 | "./tsconfig.json",
26 | "./test/tsconfig.json"
27 | ]
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Use this template to report a bug.
4 | title: "[DESCRIPTIVE BUG NAME]"
5 | labels: 🐛 Bug Report, 🔍 Needs Triage
6 | ---
7 |
8 | ### ✅ Prerequisites
9 |
10 | - [ ] Did you perform a cursory search of open issues? Is this bug already reported elsewhere?
11 | - [ ] Are you running the latest SDK version?
12 | - [ ] Are you reporting to the correct repository (`passport-magic`)?
13 |
14 | ### 🐛 Description
15 |
16 | [Description of the bug.]
17 |
18 | ### 🧩 Steps to Reproduce
19 |
20 | 1. [First Step]
21 | 2. [Second Step]
22 | 3. [and so on...]
23 |
24 | ### 🤔 Expected behavior
25 |
26 | [What you expected to happen?]
27 |
28 | ### 😮 Actual behavior
29 |
30 | [What actually happened? Please include any error stack traces you encounter.]
31 |
32 | ### 💻 Code Sample
33 |
34 | [If possible, please provide a code repository, gist, code snippet or sample files to reproduce the issue.]
35 |
36 | ### 🌎 Environment
37 |
38 | | Software | Version(s) |
39 | | ---------------- | ---------- |
40 | | `passport-magic` |
41 | | Node |
42 | | `yarn` |
43 | | Operating System |
44 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Use this template to request a new feature.
4 | title: "[DESCRIPTIVE FEATURE NAME]"
5 | labels: ✨Feature Request
6 | ---
7 |
8 | ### ✅ Prerequisites
9 |
10 | - [ ] Did you perform a cursory search of open issues? Is this feature already requested elsewhere?
11 | - [ ] Are you reporting to the correct repository (`passport-magic`)?
12 |
13 | ### ✨ Feature Request
14 |
15 | [Description of the feature.]
16 |
17 | ## 🧩 Context
18 |
19 | [Explain any additional context or rationale for this feature. What are you trying to accomplish?]
20 |
21 | ## 💻 Examples
22 |
23 | [Do you have any example(s) for the requested feature? If so, describe/demonstrate your example(s) here.]
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Use this template to request help or ask a question.
4 | title: "[WHAT'S YOUR QUESTION?]"
5 | labels: ❓Question
6 | ---
7 |
8 | ### ✅ Prerequisites
9 |
10 | - [ ] Did you perform a cursory search of open issues? Is this question already asked elsewhere?
11 | - [ ] Are you reporting to the correct repository (`passport-magic`)?
12 |
13 | ### ❓ Question
14 |
15 | [Ask your question here, please be as detailed as possible!]
16 |
17 | ### 🌎 Environment
18 |
19 | | Software | Version(s) |
20 | | ---------------- | ---------- |
21 | | `passport-magic` |
22 | | Node |
23 | | `yarn` |
24 | | Operating System |
25 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### 📦 Pull Request
2 |
3 | [Provide a general summary of the pull request here.]
4 |
5 | ### 🗜 Versioning
6 |
7 | (Check _one!_)
8 |
9 | - [ ] Patch: Bug Fix?
10 | - [ ] Minor: New Feature?
11 | - [ ] Major: Breaking Change?
12 |
13 | ### ✅ Fixed Issues
14 |
15 | - [List any fixed issues here like: Fixes #XXXX]
16 |
17 | ### 🚨 Test instructions
18 |
19 | [Describe any additional context required to test the PR/feature/bug fix.]
20 |
21 | ### ⚠️ Update `CHANGELOG.md`
22 |
23 | - [ ] I have updated the `Upcoming Changes` section of `CHANGELOG.md` with context related to this Pull Request.
24 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | concurrency:
3 | group: tests-${{ github.ref }}
4 | cancel-in-progress: true
5 | on:
6 | push:
7 | branches:
8 | - "master"
9 | pull_request:
10 |
11 | jobs:
12 | lint:
13 | runs-on: ubuntu-20.04
14 | name: Run linter and package audit
15 | steps:
16 | - uses: actions/checkout@v3
17 |
18 | - uses: actions/setup-python@v4
19 | with:
20 | python-version: '3'
21 |
22 | - name: Setup node
23 | uses: actions/setup-node@v3
24 | with:
25 | node-version: 16
26 | cache: 'yarn'
27 | - run: |
28 | yarn -v
29 | python -V
30 | yarn
31 |
32 | - name: Lint
33 | run: yarn run lint
34 |
35 | - name: Audit Production Dependencies
36 | run: yarn audit --groups dependencies || true
37 |
38 | test:
39 | runs-on: ubuntu-20.04
40 | name: Run tests
41 | steps:
42 | - uses: actions/checkout@v3
43 |
44 | - uses: actions/setup-python@v4
45 | with:
46 | python-version: '3'
47 |
48 | - name: Setup node
49 | uses: actions/setup-node@v3
50 | with:
51 | node-version: 16
52 | cache: 'yarn'
53 | - run: |
54 | yarn -v
55 | python -V
56 | yarn
57 |
58 | - name: Test
59 | run: |
60 | yarn build
61 | yarn run test
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Dependencies
4 | /node_modules
5 | /dist
6 |
7 | # Misc
8 | /.idea
9 | .DS_Store
10 | npm-debug.log*
11 | yarn-error.log*
12 | .DS_Store
13 | .vscode
14 |
15 | # Use Yarn!
16 | package-lock.json
17 |
18 | # Test artifacts
19 | /coverage
20 | /.nyc_output
21 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = require('@ikscodes/prettier-config');
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Upcoming Changes
2 |
3 | #### Fixed
4 |
5 | - ...
6 |
7 | #### Changed
8 |
9 | - ...
10 |
11 | #### Added
12 |
13 | - ...
14 |
15 | ## `1.0.0` - 04/09/2020
16 |
17 | This is the first release our changelog records. Future updates will be logged in the following format:
18 |
19 | #### Fixed
20 |
21 | - Bug fixes and patches will be described here.
22 |
23 | #### Changed
24 |
25 | - Changes (breaking or otherwise) to current APIs will be described here.
26 |
27 | #### Added
28 |
29 | - New features or APIs will be described here.
30 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, please first discuss the change you wish to make via an **issue**. This can be a feature request or a bug report. After a maintainer has triaged your issue, you are welcome to collaborate on a pull request. If your change is small or uncomplicated, you are welcome to open an issue and pull request simultaneously.
4 |
5 | Please note we have a **code of conduct**, please follow it in all your interactions with the project.
6 |
7 | ## Setting up for Local Development
8 |
9 | 1. Fork this repostiory.
10 | 2. Clone your fork.
11 | 3. Create a new branch in your local repository with the following pattern:
12 |
13 | - For bug fixes: `bug/#[issue_number]/[descriptive_bug_name]`
14 | - For features: `feature/#[issue_number]/[descriptive_feature_name]`
15 | - For chores/the rest: `chore/[descriptive_chore_name]`
16 |
17 | 4. Install dependencies with Yarn: `yarn install`
18 | 5. Start building for development: `yarn start`
19 |
20 | ## Opening a Pull Request
21 |
22 | 1. Update the **`Upcoming Changes`** section of [`CHANGELOG.md`](./CHANGELOG.md) with your fixes, changes, or additions. A maintainer will label your changes with a version number and release date once they are published.
23 | 2. Open a pull request from your fork/branch to the upstream `master` branch of _this_ repository.
24 | 3. A maintainer will review your code changes and offer feedback or suggestions if necessary. Once your changes are approved, a maintainer will merge the pull request for you and publish a release.
25 |
26 | ## Contributor Covenant Code of Conduct
27 |
28 | ### Our Pledge
29 |
30 | We as members, contributors, and leaders pledge to make participation in our
31 | community a harassment-free experience for everyone, regardless of age, body
32 | size, visible or invisible disability, ethnicity, sex characteristics, gender
33 | identity and expression, level of experience, education, socio-economic status,
34 | nationality, personal appearance, race, religion, or sexual identity
35 | and orientation.
36 |
37 | We pledge to act and interact in ways that contribute to an open, welcoming,
38 | diverse, inclusive, and healthy community.
39 |
40 | ### Our Standards
41 |
42 | Examples of behavior that contributes to a positive environment for our
43 | community include:
44 |
45 | - Demonstrating empathy and kindness toward other people
46 | - Being respectful of differing opinions, viewpoints, and experiences
47 | - Giving and gracefully accepting constructive feedback
48 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
49 | - Focusing on what is best not just for us as individuals, but for the overall community
50 |
51 | Examples of unacceptable behavior include:
52 |
53 | - The use of sexualized language or imagery, and sexual attention or advances of any kind
54 | - Trolling, insulting or derogatory comments, and personal or political attacks
55 | - Public or private harassment
56 | - Publishing others' private information, such as a physical or email address, without their explicit permission
57 | - Other conduct which could reasonably be considered inappropriate in a professional setting
58 |
59 | ### Enforcement Responsibilities
60 |
61 | Community leaders are responsible for clarifying and enforcing our standards of
62 | acceptable behavior and will take appropriate and fair corrective action in
63 | response to any behavior that they deem inappropriate, threatening, offensive,
64 | or harmful.
65 |
66 | Community leaders have the right and responsibility to remove, edit, or reject
67 | comments, commits, code, wiki edits, issues, and other contributions that are
68 | not aligned to this Code of Conduct, and will communicate reasons for moderation
69 | decisions when appropriate.
70 |
71 | ### Scope
72 |
73 | This Code of Conduct applies within all community spaces, and also applies when
74 | an individual is officially representing the community in public spaces.
75 | Examples of representing our community include using an official e-mail address,
76 | posting via an official social media account, or acting as an appointed
77 | representative at an online or offline event.
78 |
79 | ### Enforcement
80 |
81 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
82 | reported to the community leaders responsible for enforcement at [support@magic.link](mailto:support@magic.link).
83 | All complaints will be reviewed and investigated promptly and fairly.
84 |
85 | All community leaders are obligated to respect the privacy and security of the
86 | reporter of any incident.
87 |
88 | ### Enforcement Guidelines
89 |
90 | Community leaders will follow these Community Impact Guidelines in determining
91 | the consequences for any action they deem in violation of this Code of Conduct:
92 |
93 | #### 1. Correction
94 |
95 | **Community Impact**: Use of inappropriate language or other behavior deemed
96 | unprofessional or unwelcome in the community.
97 |
98 | **Consequence**: A private, written warning from community leaders, providing
99 | clarity around the nature of the violation and an explanation of why the
100 | behavior was inappropriate. A public apology may be requested.
101 |
102 | #### 2. Warning
103 |
104 | **Community Impact**: A violation through a single incident or series
105 | of actions.
106 |
107 | **Consequence**: A warning with consequences for continued behavior. No
108 | interaction with the people involved, including unsolicited interaction with
109 | those enforcing the Code of Conduct, for a specified period of time. This
110 | includes avoiding interactions in community spaces as well as external channels
111 | like social media. Violating these terms may lead to a temporary or
112 | permanent ban.
113 |
114 | #### 3. Temporary Ban
115 |
116 | **Community Impact**: A serious violation of community standards, including
117 | sustained inappropriate behavior.
118 |
119 | **Consequence**: A temporary ban from any sort of interaction or public
120 | communication with the community for a specified period of time. No public or
121 | private interaction with the people involved, including unsolicited interaction
122 | with those enforcing the Code of Conduct, is allowed during this period.
123 | Violating these terms may lead to a permanent ban.
124 |
125 | #### 4. Permanent Ban
126 |
127 | **Community Impact**: Demonstrating a pattern of violation of community
128 | standards, including sustained inappropriate behavior, harassment of an
129 | individual, or aggression toward or disparagement of classes of individuals.
130 |
131 | **Consequence**: A permanent ban from any sort of public interaction within
132 | the community.
133 |
134 | ### Attribution
135 |
136 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
137 | version 2.0, available at
138 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
139 |
140 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
141 | enforcement ladder](https://github.com/mozilla/diversity).
142 |
143 | For answers to common questions about this code of conduct, see the FAQ at
144 | https://www.contributor-covenant.org/faq. Translations are available at
145 | https://www.contributor-covenant.org/translations.
146 |
147 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Fortmatic Inc.
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 | # Magic Authentication For Passport JS
2 |
3 | [](https://github.com/magiclabs/passport-magic/actions/workflows/test.yaml)
4 |
5 | > Integrate [Magic](https://magic.link) passwordless authentication with your Passport.js application.
6 |
7 |
8 | License ·
9 | Changelog ·
10 | Contributing Guide
11 |
12 |
13 | ## 📖 Documentation
14 |
15 | See the [developer documentation](https://docs.magic.link/tutorials/full-stack-node-js) to learn how you can integrate Magic into your Passport.js application in a matter of minutes.
16 |
17 | ## 🔗 Installation
18 |
19 | Integrating your Passport.js application with Magic will require our server-side NPM package:
20 |
21 | ```bash
22 | # Via NPM:
23 | npm install --save passport-magic
24 |
25 | # Via Yarn:
26 | yarn add passport-magic
27 | ```
28 |
29 | ## ⚡️ Quick Start
30 |
31 | ```ts
32 | const passport = require("passport");
33 | const MagicStrategy = require("passport-magic").Strategy;
34 |
35 | const strategy = new MagicStrategy(async function(user, done) {
36 | const userMetadata = await magic.users.getMetadataByIssuer(user.issuer);
37 | const existingUser = await users.findOne({ issuer: user.issuer });
38 | if (!existingUser) {
39 | /* Create new user if doesn't exist */
40 | return signup(user, userMetadata, done);
41 | } else {
42 | /* Login user if otherwise */
43 | return login(user, done);
44 | }
45 | });
46 |
47 | passport.use(strategy);
48 | ```
49 |
--------------------------------------------------------------------------------
/config/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["es2018"],
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "target": "es5",
7 | "strict": true,
8 | "allowSyntheticDefaultImports": true,
9 | "experimentalDecorators": true,
10 | "noImplicitReturns": true,
11 | "noImplicitThis": true,
12 | "esModuleInterop": true,
13 | "downlevelIteration": true,
14 | "resolveJsonModule": true,
15 | "allowJs": true,
16 | "sourceMap": true,
17 | "declaration": true,
18 | },
19 | "include": ["../src/**/*.ts"],
20 | "exclude": ["../node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/config/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "../dist",
5 | },
6 | "include": ["../src/**/*.ts"]
7 | }
8 |
--------------------------------------------------------------------------------
/config/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "target": "es6",
6 | "strict": false,
7 | "noImplicitAny": false,
8 | "downlevelIteration": true,
9 | "esModuleInterop": true
10 | },
11 | "include": ["../test/**/*.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "passport-magic",
3 | "version": "1.0.0",
4 | "description": "Passport JS strategy for authentication with Magic.",
5 | "author": "Fortmatic (https://fortmatic.com/)",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/fortmatic/passport-magic"
10 | },
11 | "main": "dist/index.js",
12 | "types": "dist/index.d.ts",
13 | "scripts": {
14 | "start": "npm run clean:build && ./scripts/start.sh",
15 | "build": "npm run clean:build && ./scripts/build.sh",
16 | "test": "npm run clean:test-artifacts && ./scripts/test.sh",
17 | "lint": "eslint --fix src/**/*.ts",
18 | "clean": "npm-run-all -s clean:*",
19 | "clean:test-artifacts": "rimraf coverage && rimraf .nyc_output",
20 | "clean:build": "rimraf dist",
21 | "clean_node_modules": "rimraf node_modules"
22 | },
23 | "devDependencies": {
24 | "@ikscodes/eslint-config": "^6.2.0",
25 | "@ikscodes/prettier-config": "^1.0.0",
26 | "@istanbuljs/nyc-config-typescript": "^1.0.1",
27 | "@types/eth-sig-util": "^2.1.0",
28 | "@types/express": "^4.17.2",
29 | "@types/node": "^13.1.2",
30 | "@types/node-fetch": "^2.5.4",
31 | "@types/passport-local": "^1.0.33",
32 | "@types/passport-strategy": "^0.2.35",
33 | "@types/sinon": "^7.5.2",
34 | "@typescript-eslint/eslint-plugin": "^2.15.0",
35 | "ava": "^3.4.0",
36 | "eslint": "^6.7.2",
37 | "eslint-import-resolver-typescript": "^2.0.0",
38 | "eslint-plugin-import": "^2.18.2",
39 | "eslint-plugin-jsx-a11y": "^6.2.3",
40 | "eslint-plugin-prettier": "^3.1.2",
41 | "eslint-plugin-react": "^7.15.1",
42 | "eslint-plugin-react-hooks": "^1.7.0",
43 | "husky": "^4.2.3",
44 | "lint-staged": "^10.0.8",
45 | "npm-run-all": "~4.1.5",
46 | "nyc": "^15.0.0",
47 | "prettier": "^1.19.1",
48 | "rimraf": "~3.0.0",
49 | "sinon": "^9.0.0",
50 | "ts-node": "~8.5.2",
51 | "tslint": "~5.20.1",
52 | "typescript": "~3.8.3"
53 | },
54 | "dependencies": {
55 | "@magic-sdk/admin": "^1.0.0",
56 | "express": "^4.17.1",
57 | "passport": "^0.4.1",
58 | "passport-local": "^1.0.0",
59 | "passport-strategy": "^1.0.0"
60 | },
61 | "husky": {
62 | "hooks": {
63 | "pre-commit": "lint-staged"
64 | }
65 | },
66 | "lint-staged": {
67 | "*.{ts,tsx}": "eslint --fix"
68 | },
69 | "ava": {
70 | "require": [
71 | "ts-node/register"
72 | ],
73 | "files": [
74 | "test/**/*.spec.ts"
75 | ],
76 | "extensions": [
77 | "ts"
78 | ],
79 | "verbose": true
80 | },
81 | "nyc": {
82 | "extends": "@istanbuljs/nyc-config-typescript",
83 | "all": false,
84 | "check-coverage": true,
85 | "per-file": true,
86 | "lines": 99,
87 | "statements": 99,
88 | "functions": 99,
89 | "branches": 99,
90 | "reporter": [
91 | "html",
92 | "lcov"
93 | ]
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo
4 | echo "Building passport-magic for production."
5 | echo
6 |
7 | # Increase memory limit for Node
8 | export NODE_OPTIONS=--max_old_space_size=4096
9 |
10 | export NODE_ENV=production
11 |
12 | npx tsc -p ./config/tsconfig.lib.json
13 |
--------------------------------------------------------------------------------
/scripts/start.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo
4 | echo "Building passport-magic for development."
5 | echo
6 |
7 | # Increase memory limit for Node
8 | export NODE_OPTIONS=--max_old_space_size=4096
9 |
10 | export NODE_ENV=development
11 |
12 | npx tsc -w -p ./config/tsconfig.lib.json
13 |
--------------------------------------------------------------------------------
/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo
4 | echo "Running unit tests..."
5 | echo
6 |
7 | # Increase memory limit for Node
8 | export NODE_OPTIONS=--max_old_space_size=4096
9 |
10 | export NODE_ENV=test
11 |
12 | if [ -n "$1" ]; then
13 | input=$(echo $(npx glob $1))
14 | fi
15 |
16 | export TS_NODE_PROJECT="test/tsconfig.json"
17 | npx nyc --reporter=lcov --reporter=text-summary ava $input
18 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './strategy';
2 | export * from './types';
3 |
--------------------------------------------------------------------------------
/src/strategy.ts:
--------------------------------------------------------------------------------
1 | /*
2 | eslint-disable
3 |
4 | no-template-curly-in-string,
5 | no-multi-assign,
6 | prefer-rest-params
7 | */
8 |
9 | import { Strategy as BaseStrategy } from 'passport-strategy';
10 | import { Magic, SDKError as MagicSDKError } from '@magic-sdk/admin';
11 | import { Request } from 'express';
12 | import { VerifyFunc, VerifyFuncWithReq, StrategyOptions, StrategyOptionsWithReq, MagicUser, DoneFunc } from './types';
13 |
14 | export class Strategy extends BaseStrategy {
15 | public readonly name = 'magic';
16 | private readonly verify: VerifyFunc;
17 | private readonly verifyWithReq: VerifyFuncWithReq;
18 | private readonly passReqToCallback: boolean;
19 | private readonly attachmentAttribute: string;
20 | private readonly magicInstance: Magic;
21 |
22 | /**
23 | * Creates an instance of `MagicStrategy`.
24 | *
25 | * This authentication strategy validates requests based on an authorization
26 | * header containing a Decentralized ID Token (DIDT).
27 | *
28 | * Applications must supply a `verify` callback which accepts a `MagicUser`
29 | * object with the following information:
30 | *
31 | * 1. `claim`: The validated and parsed DIDT claim.
32 | * 2. `id`: The user's Decentralized Identfier. This should be used as the
33 | * ID column in your user tables.
34 | * 3. `publicAddress`: The public address of the signing user. DIDTs are
35 | * generated using Elliptic Curve public/private key pairs.
36 | *
37 | * The `verify` callback also supplies a `done` callback, which should be
38 | * called with the user's resolved profile information or set to `false` if
39 | * the credentials are not valid (i.e.: due to a replay attack).
40 | *
41 | * If an exception occurred, `err` should be set.
42 | *
43 | * An `options` object can be passed to the constructor to customize behavior of the `verify` callback:
44 | *
45 | * Options:
46 | * - `magicInstance`: A custom Magic SDK instance to use.
47 | * - `passReqToCallback`: When `true`, `req` is the first argument to the verify callback (default: `false`).
48 | *
49 | * **NOTE: Parameters can be provided in any order!**
50 | *
51 | * @param options - Options to customize the functionality of `verify`.
52 | * @param verify - A callback to validate the authentication request.
53 | *
54 | * @see https://docs.magic.link/tutorials/decentralized-id
55 | * @see https://w3c-ccg.github.io/did-primer/
56 | *
57 | * @example
58 | * passport.use(new MagicStrategy(
59 | * ({ id }, done) => {
60 | * try {
61 | * const user = await User.findOne(id);
62 | * done(null, user);
63 | * } catch (err) {
64 | * done(err);
65 | * }
66 | * }
67 | * ));
68 | */
69 | /* eslint-disable prettier/prettier */
70 | constructor(options: StrategyOptions, verify: VerifyFunc);
71 | constructor(options: StrategyOptionsWithReq, verify: VerifyFuncWithReq);
72 | constructor(verify: VerifyFunc, options: StrategyOptions);
73 | constructor(verify: VerifyFuncWithReq, options: StrategyOptionsWithReq);
74 | constructor(verify: VerifyFunc);
75 | /* eslint-enable prettier/prettier */
76 | constructor(
77 | arg0: VerifyFunc | VerifyFuncWithReq | StrategyOptions | StrategyOptionsWithReq,
78 | arg1?: VerifyFunc | VerifyFuncWithReq | StrategyOptions | StrategyOptionsWithReq,
79 | ) {
80 | super();
81 |
82 | // Extract options from arguments -- parameters can be provided in any order.
83 | const args = Array.from(arguments);
84 | const verify = args.find(arg => typeof arg === 'function') as VerifyFunc | VerifyFuncWithReq;
85 | const options = args.find(arg => typeof arg !== 'function') as StrategyOptions | StrategyOptionsWithReq | undefined;
86 |
87 | if (!verify) throw new TypeError('`MagicStrategy` requires a `verify` callback.');
88 |
89 | this.verify = this.verifyWithReq = verify as any;
90 | this.passReqToCallback = !!options?.passReqToCallback;
91 | this.attachmentAttribute = options?.attachmentAttribute ?? 'attachment';
92 | this.magicInstance = options?.magicInstance || new Magic();
93 | }
94 |
95 | /**
96 | * Authenticate request based on the authorization header.
97 | *
98 | * @param req - A request object from Express.
99 | */
100 | public async authenticate(req: Request) {
101 | const hasAuthorizationHeader = !!req.headers.authorization;
102 | const isFormattedCorrectly = req.headers.authorization?.toLowerCase().startsWith('bearer ');
103 |
104 | if (!hasAuthorizationHeader) return this.fail({ message: 'Missing authorization header.' }, 400);
105 | if (!isFormattedCorrectly) {
106 | return this.fail({ message: 'Malformed authorization header. Please use the `Bearer ${token}` format.' }, 400);
107 | }
108 |
109 | const didToken = req.headers.authorization!.substring(7);
110 | const attachment = (req as any)[this.attachmentAttribute] ?? 'none';
111 |
112 | try {
113 | this.magicInstance.token.validate(didToken, attachment);
114 | const user: MagicUser = {
115 | issuer: this.magicInstance.token.getIssuer(didToken),
116 | publicAddress: this.magicInstance.token.getPublicAddress(didToken),
117 | claim: this.magicInstance.token.decode(didToken)[1],
118 | };
119 |
120 | const done: DoneFunc = (_err, _user, _info: any) => {
121 | if (_err) return this.error(_err);
122 | if (!_user) return this.fail(_info);
123 | this.success(_user, _info);
124 | };
125 |
126 | try {
127 | if (this.passReqToCallback) this.verifyWithReq(req, user, done);
128 | else this.verify(user, done);
129 | } catch (err) {
130 | return this.error(err);
131 | }
132 | } catch (err) {
133 | if (err instanceof MagicSDKError) return this.fail({ message: err.message, error_code: err.code }, 401);
134 | return this.fail({ message: 'Invalid DID token.' }, 401);
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { Magic, Claim } from '@magic-sdk/admin';
2 | import { Request } from 'express';
3 |
4 | export interface StrategyOptionsWithReq {
5 | magicInstance?: Magic;
6 | passReqToCallback?: true;
7 | attachmentAttribute?: string;
8 | }
9 |
10 | export interface StrategyOptions {
11 | magicInstance?: Magic;
12 | passReqToCallback?: false;
13 | attachmentAttribute?: string;
14 | }
15 |
16 | export interface AuthenticateOptions {
17 | attachment?: string;
18 | }
19 |
20 | export interface MagicUser {
21 | issuer: string;
22 | publicAddress: string;
23 | claim: Claim;
24 | }
25 |
26 | export interface DoneFuncInfo {
27 | message: string;
28 | }
29 |
30 | export interface DoneFunc {
31 | (error: any, user?: any, info?: DoneFuncInfo): void;
32 | }
33 |
34 | export interface VerifyFuncWithReq {
35 | (req: Request, user: MagicUser, done: DoneFunc): void;
36 | }
37 |
38 | export interface VerifyFunc {
39 | (user: MagicUser, done: DoneFunc): void;
40 | }
41 |
--------------------------------------------------------------------------------
/test/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const VALID_DIDT =
2 | 'WyIweDE5YTIyMzg5MzZhNDAwYmE0MzZhZWFlNGYyMjJkMTNkYmM4MGNmODJkZDdlYTBiODU1NWVmMDU0NDY0NDc2ODMzMWI3MTBhOGJhZWNmYjljNmM5MzU0MGYwOWU4YWIzZmFmMWI0NTAyOTBiMTQ1ZjQ3NTRmZWJhNDIwMWE5ZmJhMWIiLCJ7XCJpYXRcIjoxNTg1MDE1ODQ5LFwiZXh0XCI6MTkwMDQxNTg0OSxcImlzc1wiOlwiZGlkOmV0aHI6MHhCMmVjOWI2MTY5OTc2MjQ5MWI2NTQyMjc4RTlkRkVDOTA1MGY4MDg5XCIsXCJzdWJcIjpcIjZ0RlhUZlJ4eWt3TUtPT2pTTWJkUHJFTXJwVWwzbTNqOERReWNGcU8ydHc9XCIsXCJhdWRcIjpcImRpZDptYWdpYzoxYjNhNTcwZS04ZmUzLTQ0MTItYmU3MS1mNDg2ZjI3ZWE3YWZcIixcIm5iZlwiOjE1ODUwMTU4NDksXCJ0aWRcIjpcIjRmMzAxZDIzLTBjNDAtNDhmMS1hNTIyLTQxOTQwOTFmZjIyOFwiLFwiYWRkXCI6XCIweDkxZmJlNzRiZTZjNmJmZDhkZGRkZDkzMDExYjA1OWI5MjUzZjEwNzg1NjQ5NzM4YmEyMTdlNTFlMGUzZGYxMzgxZDIwZjUyMWEzNjQxZjIzZWI5OWNjYjM0ZTNiYzVkOTYzMzJmZGViYzhlZmE1MGNkYjQxNWU0NTUwMDk1MmNkMWNcIn0iXQ==';
3 |
4 | export const VALID_DIDT_PARSED_CLAIMS = {
5 | iat: 1585015849,
6 | ext: 1900415849,
7 | iss: 'did:ethr:0xB2ec9b61699762491b6542278E9dFEC9050f8089',
8 | sub: '6tFXTfRxykwMKOOjSMbdPrEMrpUl3m3j8DQycFqO2tw=',
9 | aud: 'did:magic:1b3a570e-8fe3-4412-be71-f486f27ea7af',
10 | nbf: 1585015849,
11 | tid: '4f301d23-0c40-48f1-a522-4194091ff228',
12 | add:
13 | '0x91fbe74be6c6bfd8ddddd93011b059b9253f10785649738ba217e51e0e3df1381d20f521a3641f23eb99ccb34e3bc5d96332fdebc8efa50cdb415e45500952cd1c',
14 | };
15 |
16 | export const VALID_DIDT_DECODED = [
17 | '0x19a2238936a400ba436aeae4f222d13dbc80cf82dd7ea0b8555ef0544644768331b710a8baecfb9c6c93540f09e8ab3faf1b450290b145f4754feba4201a9fba1b',
18 | VALID_DIDT_PARSED_CLAIMS,
19 | ];
20 |
21 | export const VALID_DIDT_WITH_ATTACHMENT =
22 | 'WyIweDA4MWMzODBlMzc5NTdjNjZiZmIwYTUwMzI4YWJiMmZlY2E3OTUzNjVhZGFlMjhkNDU5NTFiMzg3MzI2OTM1NTgwMTQ4NjA0NTYwZGIwMDRhOTc2YTZmZjA4NWUxODExNmMwZGI5OWE2MWNiMjg0NDcxNDQ4MzVlODI2ZjIwOTI5MWIiLCJ7XCJpYXRcIjoxNTg1MDE2MzA3LFwiZXh0XCI6MTkwMDQxNjMwNyxcImlzc1wiOlwiZGlkOmV0aHI6MHhCMmVjOWI2MTY5OTc2MjQ5MWI2NTQyMjc4RTlkRkVDOTA1MGY4MDg5XCIsXCJzdWJcIjpcIjZ0RlhUZlJ4eWt3TUtPT2pTTWJkUHJFTXJwVWwzbTNqOERReWNGcU8ydHc9XCIsXCJhdWRcIjpcImRpZDptYWdpYzo1OGYxOTdhNy1hMDVhLTQ4YTEtYjhkZi0xZjgyMjdjNTZhZTJcIixcIm5iZlwiOjE1ODUwMTYzMDcsXCJ0aWRcIjpcImY1ZmE4ZTA5LWZjMDMtNDBhZC05MGVhLTQ2YmNkZDg1YWExYlwiLFwiYWRkXCI6XCIweGM1MGI4M2U1ZGEzOGY1ODc1NzcwOTM4ZGZhNjU1N2M0ZDRmYTM3ZTc1MTMxZWE0NjA5MDVkOGNlYzZiOGNhMjcxNmRlNzExM2Q1YjQ1ZTc2NjI3MmFlMTMwMzc2NzYzYjRiNjA4NWVhNGU3MGNiOTQ2YjlkNDU3MmYzZmJiYWJjMWJcIn0iXQ==';
23 |
24 | export const VALID_DIDT_WITH_ATTACHMENT_PARSED_CLAIMS = {
25 | iat: 1585016307,
26 | ext: 1900416307,
27 | iss: 'did:ethr:0xB2ec9b61699762491b6542278E9dFEC9050f8089',
28 | sub: '6tFXTfRxykwMKOOjSMbdPrEMrpUl3m3j8DQycFqO2tw=',
29 | aud: 'did:magic:58f197a7-a05a-48a1-b8df-1f8227c56ae2',
30 | nbf: 1585016307,
31 | tid: 'f5fa8e09-fc03-40ad-90ea-46bcdd85aa1b',
32 | add:
33 | '0xc50b83e5da38f5875770938dfa6557c4d4fa37e75131ea460905d8cec6b8ca2716de7113d5b45e766272ae130376763b4b6085ea4e70cb946b9d4572f3fbbabc1b',
34 | };
35 |
36 | export const VALID_DIDT_WITH_ATTACHMENT_DECODED = [
37 | '0x081c380e37957c66bfb0a50328abb2feca795365adae28d45951b387326935580148604560db004a976a6ff085e18116c0db99a61cb28447144835e826f209291b',
38 | VALID_DIDT_WITH_ATTACHMENT_PARSED_CLAIMS,
39 | ];
40 |
41 | export const EXPIRED_DIDT =
42 | 'WyIweDM1MjcyZmExYmU5NGViODY3MTgxMDdlYmYxM2Y2YmYzYzE5MTE1MGJjZTM1NmYwNDlmM2NlMTRhMjUwMjk1ZjA0NGVhMDBjZDcxMGYxYjhiMTZlYjdiNzRkNzdjOTk2ODRjN2JkNDNmMGFhOTJmNTk1MTU4ZWFhMDkwZDZlNWI5MWMiLCJ7XCJpYXRcIjoxNTg1MDE1ODg3LFwiZXh0XCI6MTU4NTAxNTg4OCxcImlzc1wiOlwiZGlkOmV0aHI6MHhCMmVjOWI2MTY5OTc2MjQ5MWI2NTQyMjc4RTlkRkVDOTA1MGY4MDg5XCIsXCJzdWJcIjpcIjZ0RlhUZlJ4eWt3TUtPT2pTTWJkUHJFTXJwVWwzbTNqOERReWNGcU8ydHc9XCIsXCJhdWRcIjpcImRpZDptYWdpYzoyMTFjZTZhZC05ZjAyLTQ5NTMtODU3NC1jMGM2N2QyNThjMzRcIixcIm5iZlwiOjE1ODUwMTU4ODcsXCJ0aWRcIjpcImVkNGZiMWQ2LTZjYjEtNGQ5MS04ZmI3LTAyZGQ0YTYxZjMwM1wiLFwiYWRkXCI6XCIweDkxZmJlNzRiZTZjNmJmZDhkZGRkZDkzMDExYjA1OWI5MjUzZjEwNzg1NjQ5NzM4YmEyMTdlNTFlMGUzZGYxMzgxZDIwZjUyMWEzNjQxZjIzZWI5OWNjYjM0ZTNiYzVkOTYzMzJmZGViYzhlZmE1MGNkYjQxNWU0NTUwMDk1MmNkMWNcIn0iXQ==';
43 |
--------------------------------------------------------------------------------
/test/lib/factories.ts:
--------------------------------------------------------------------------------
1 | import sinon from 'sinon';
2 | import { Strategy } from '../../src/strategy';
3 |
4 | export function createStrategyInstance(
5 | options: {
6 | passReqToCallback?: boolean;
7 | shouldFailVerification?: boolean;
8 | shouldErrorVerification?: boolean;
9 | shouldThrowVerification?: boolean;
10 | } = {} as any,
11 | ) {
12 | const optionsWithDefaults = {
13 | passReqToCallback: false,
14 | shouldFailVerification: false,
15 | shouldErrorVerification: false,
16 | shouldThrowVerification: false,
17 | ...options,
18 | };
19 |
20 | const verifyStub = sinon.spy((user, done) => {
21 | if (optionsWithDefaults.shouldThrowVerification) throw new Error('uh oh!');
22 |
23 | done(
24 | optionsWithDefaults.shouldErrorVerification ? { message: 'hello world' } : null,
25 | optionsWithDefaults.shouldFailVerification ? false : user,
26 | optionsWithDefaults.shouldFailVerification ? { message: 'goodbye world' } : undefined,
27 | );
28 | });
29 |
30 | const verifyStubWithReq = sinon.spy((req, user, done) => {
31 | if (optionsWithDefaults.shouldThrowVerification) throw new Error('uh oh!');
32 |
33 | done(
34 | optionsWithDefaults.shouldErrorVerification ? { message: 'hello world' } : null,
35 | optionsWithDefaults.shouldFailVerification ? false : user,
36 | optionsWithDefaults.shouldFailVerification ? { message: 'goodbye world' } : undefined,
37 | );
38 | });
39 |
40 | const strat = new Strategy(optionsWithDefaults.passReqToCallback ? verifyStubWithReq : verifyStub, {
41 | passReqToCallback: optionsWithDefaults.passReqToCallback,
42 | } as any);
43 |
44 | const failStub = sinon.stub();
45 | const errorStub = sinon.stub();
46 | const successStub = sinon.stub();
47 |
48 | strat.fail = failStub;
49 | strat.error = errorStub;
50 | strat.success = successStub;
51 |
52 | return {
53 | verifyStub,
54 | verifyStubWithReq,
55 | strat,
56 | failStub,
57 | errorStub,
58 | successStub,
59 | };
60 | }
61 |
--------------------------------------------------------------------------------
/test/spec/strategy/authenticate.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-template-curly-in-string */
2 |
3 | import test from 'ava';
4 | import sinon from 'sinon';
5 | import { ErrorCode as MagicSDKErrorCode } from '@magic-sdk/admin';
6 | import {
7 | VALID_DIDT,
8 | VALID_DIDT_PARSED_CLAIMS,
9 | EXPIRED_DIDT,
10 | VALID_DIDT_WITH_ATTACHMENT,
11 | VALID_DIDT_WITH_ATTACHMENT_PARSED_CLAIMS,
12 | } from '../../lib/constants';
13 | import { createStrategyInstance } from '../../lib/factories';
14 |
15 | const invalidReq: any = { headers: { authorization: `Bearer ${EXPIRED_DIDT}` } };
16 |
17 | const validReq: any = { headers: { authorization: `Bearer ${VALID_DIDT}` } };
18 | const validUser: any = {
19 | issuer: VALID_DIDT_PARSED_CLAIMS.iss,
20 | publicAddress: VALID_DIDT_PARSED_CLAIMS.iss.split(':')[2],
21 | claim: VALID_DIDT_PARSED_CLAIMS,
22 | };
23 |
24 | const validReqWithAttachment: any = {
25 | headers: { authorization: `Bearer ${VALID_DIDT_WITH_ATTACHMENT}` },
26 | attachment: 'asdf',
27 | };
28 | const validUserWithAttachment: any = {
29 | issuer: VALID_DIDT_WITH_ATTACHMENT_PARSED_CLAIMS.iss,
30 | publicAddress: VALID_DIDT_WITH_ATTACHMENT_PARSED_CLAIMS.iss.split(':')[2],
31 | claim: VALID_DIDT_WITH_ATTACHMENT_PARSED_CLAIMS,
32 | };
33 |
34 | test('#01: Fails with status 400 if authorization header is missing', async t => {
35 | const { strat, verifyStub, failStub } = createStrategyInstance();
36 |
37 | await strat.authenticate({ headers: { authorization: undefined } } as any);
38 |
39 | t.true(failStub.calledOnceWith({ message: 'Missing authorization header.' }, 400));
40 | t.true(verifyStub.notCalled);
41 | });
42 |
43 | test('#02: Fails with status 400 if authorization header is malformed', async t => {
44 | const { strat, verifyStub, failStub } = createStrategyInstance();
45 |
46 | await strat.authenticate({ headers: { authorization: `notarealtoken` } } as any);
47 |
48 | t.true(
49 | failStub.calledOnceWith(
50 | { message: 'Malformed authorization header. Please use the `Bearer ${token}` format.' },
51 | 400,
52 | ),
53 | );
54 | t.true(verifyStub.notCalled);
55 | });
56 |
57 | test('#03: Succeeds validation with a valid DIDT', async t => {
58 | const { strat, verifyStub, verifyStubWithReq, successStub } = createStrategyInstance();
59 |
60 | await strat.authenticate(validReq);
61 |
62 | t.true(verifyStubWithReq.notCalled);
63 | t.deepEqual(verifyStub.args[0][0], validUser);
64 | t.true(successStub.calledOnceWith(validUser, undefined));
65 | });
66 |
67 | test('#04: Succeeds validation with a valid DIDT and `passReqToCallback` is `true`', async t => {
68 | const { strat, verifyStub, verifyStubWithReq, successStub } = createStrategyInstance({ passReqToCallback: true });
69 |
70 | await strat.authenticate(validReq);
71 |
72 | t.true(verifyStub.notCalled);
73 | t.deepEqual(verifyStubWithReq.args[0][0], validReq);
74 | t.deepEqual(verifyStubWithReq.args[0][1], validUser);
75 | t.true(successStub.calledOnceWith(validUser, undefined));
76 | });
77 |
78 | test('#05: Handles failure case from user-provided verification function', async t => {
79 | const { strat, verifyStub, failStub } = createStrategyInstance({ shouldFailVerification: true });
80 |
81 | await strat.authenticate(validReq);
82 |
83 | t.deepEqual(verifyStub.args[0][0], validUser);
84 | t.true(failStub.calledOnceWith({ message: 'goodbye world' }));
85 | });
86 |
87 | test('#06: Uses attachment from `req[attachmentAttribute]`', async t => {
88 | const { strat, verifyStub, failStub } = createStrategyInstance({ shouldFailVerification: true });
89 |
90 | await strat.authenticate(validReqWithAttachment);
91 |
92 | t.deepEqual(verifyStub.args[0][0], validUserWithAttachment);
93 | t.true(failStub.calledOnceWith({ message: 'goodbye world' }));
94 | });
95 |
96 | test('#07: Handles error case from user-provided verification function', async t => {
97 | const { strat, verifyStub, errorStub } = createStrategyInstance({ shouldErrorVerification: true });
98 |
99 | await strat.authenticate(validReq);
100 |
101 | t.deepEqual(verifyStub.args[0][0], validUser);
102 | t.true(errorStub.calledOnceWith({ message: 'hello world' }));
103 | });
104 |
105 | test('#08: Handles exception while executing user-provided verification function', async t => {
106 | const { strat, verifyStub, errorStub } = createStrategyInstance({ shouldThrowVerification: true });
107 |
108 | await strat.authenticate(validReq);
109 |
110 | t.deepEqual(verifyStub.args[0][0], validUser);
111 | t.is(errorStub.args[0][0].message, 'uh oh!');
112 | });
113 |
114 | test('#09: Handles exceptions from Magic Admin SDK during token validation', async t => {
115 | const { strat, verifyStub, failStub } = createStrategyInstance();
116 |
117 | await strat.authenticate(invalidReq);
118 |
119 | t.true(verifyStub.notCalled);
120 | t.true(Object.values(MagicSDKErrorCode).includes(failStub.args[0][0].error_code));
121 | t.is(failStub.args[0][1], 401);
122 | });
123 |
124 | test('#10: Handles generic exceptions during token validation', async t => {
125 | const { strat, verifyStub, failStub } = createStrategyInstance();
126 |
127 | (strat as any).magicInstance.token.validate = sinon.spy(() => {
128 | throw new Error();
129 | });
130 | await strat.authenticate(validReq);
131 |
132 | t.true(verifyStub.notCalled);
133 | t.true(failStub.calledWith({ message: 'Invalid DID token.' }, 401));
134 | });
135 |
--------------------------------------------------------------------------------
/test/spec/strategy/constructor.spec.ts:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import { Strategy as BaseStrategy } from 'passport-strategy';
3 | import { Magic } from '@magic-sdk/admin';
4 | import { Strategy } from '../../../src/strategy';
5 |
6 | test('#01: Initialize `MagicStrategy`', t => {
7 | const verify = () => {};
8 | const strat = new Strategy(verify);
9 |
10 | t.true(strat instanceof BaseStrategy);
11 | t.is(strat.name, 'magic');
12 | t.is((strat as any).verify, verify);
13 | t.is((strat as any).verifyWithReq, verify);
14 | t.is((strat as any).attachmentAttribute, 'attachment');
15 | t.true((strat as any).magicInstance instanceof Magic);
16 | t.false((strat as any).passReqToCallback);
17 | });
18 |
19 | test('#02: Initialize `MagicStrategy` with custom Magic Admin SDK instance', t => {
20 | const customMagicInst = new Magic('API_KEY');
21 | const strat = new Strategy({ magicInstance: customMagicInst }, () => {});
22 |
23 | t.is((strat as any).magicInstance, customMagicInst);
24 | });
25 |
26 | test('#03: Initialize `MagicStrategy` with custom attachment attribute name', t => {
27 | const customMagicInst = new Magic('API_KEY');
28 | const strat = new Strategy({ attachmentAttribute: 'foobar' }, () => {});
29 |
30 | t.is((strat as any).attachmentAttribute, 'foobar');
31 | });
32 |
33 | test('#04: Fail to initialize `MagicStrategy` without a `verify` callback', t => {
34 | // Given `undefined` as only argument.
35 | t.throws(() => new Strategy(undefined), {
36 | instanceOf: TypeError,
37 | message: '`MagicStrategy` requires a `verify` callback.',
38 | });
39 |
40 | // Given `undefined` as first argument.
41 | t.throws(() => new Strategy(undefined, {}), {
42 | instanceOf: TypeError,
43 | message: '`MagicStrategy` requires a `verify` callback.',
44 | });
45 |
46 | // Given `undefined` as second argument.
47 | t.throws(() => new Strategy({}, undefined), {
48 | instanceOf: TypeError,
49 | message: '`MagicStrategy` requires a `verify` callback.',
50 | });
51 | });
52 |
53 | test('#05: Arguments can be provided in any order', t => {
54 | const verify = () => {};
55 | const options = { passReqToCallback: true } as const;
56 |
57 | // With `verify` as first argument.
58 | const strat1 = new Strategy(verify, options);
59 | t.is((strat1 as any).verify, verify);
60 | t.is((strat1 as any).verifyWithReq, verify);
61 | t.true((strat1 as any).passReqToCallback);
62 |
63 | // With `verify` as second argument.
64 | const strat2 = new Strategy(options, verify);
65 | t.is((strat2 as any).verify, verify);
66 | t.is((strat2 as any).verifyWithReq, verify);
67 | t.true((strat2 as any).passReqToCallback);
68 | });
69 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../config/tsconfig.test.json"
3 | }
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./config/tsconfig.base.json"
3 | }
4 |
--------------------------------------------------------------------------------