├── .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 | [![Tests](https://github.com/magiclabs/passport-magic/actions/workflows/test.yaml/badge.svg)](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 | --------------------------------------------------------------------------------