├── .prettierrc ├── .gitignore ├── tsconfig.json ├── package.json ├── LICENSE ├── README.md └── index.ts /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "all", 4 | "semi": false 5 | } 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .dfx 7 | todo.txt 8 | canister_ids.json 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "CommonJS", 5 | "moduleResolution": "Node", 6 | "outDir": "./dist", 7 | "declaration": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "skipLibCheck": true 12 | }, 13 | "include": ["index.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ic-auth", 3 | "version": "1.1.0", 4 | "description": "A simple, modular package for integrating Internet Computer authentication providers into your app.", 5 | "author": "Daniel McCoy (https://danielmccoy.us/)", 6 | "license": "MIT", 7 | "keywords": [ 8 | "ic", 9 | "internet computer", 10 | "auth", 11 | "authentication", 12 | "identity", 13 | "stoic", 14 | "dfinity" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/id-daniel-mccoy/ic-auth.git" 19 | }, 20 | "main": "dist/index.js", 21 | "types": "dist/index.d.ts", 22 | "scripts": { 23 | "build": "tsc" 24 | }, 25 | "dependencies": { 26 | "@dfinity/agent": "2.4.1", 27 | "@dfinity/assets": "^2.4.1", 28 | "@dfinity/auth-client": "2.4.1", 29 | "@dfinity/candid": "2.4.1", 30 | "@dfinity/identity": "2.4.1", 31 | "@dfinity/principal": "2.4.1", 32 | "@nfid/identitykit": "^1.0.14", 33 | "ic-stoic-identity": "6.0.0" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^16.11.6", 37 | "typescript": "^4.4.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024–2025 Daniel McCoy 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 | # IC-Auth 2 | ## Version 1.1.0 3 | 4 | 5 |
6 | 7 | **Welcome to IC-Auth! The easiest way to integrate Plug, Stoic, Internet Identity, and NFID authentication for your Internet Computer apps.** 8 | 9 |
10 | 11 | ## 📌 What can you do with IC-Auth? 12 | 13 | **IC-Auth** is a lightweight, modular TypeScript package that helps developers plug wallet authentication directly into their DFINITY dapps.\ 14 | It gives you simple, type-safe helpers for the **most common IC wallets**, so you can focus on your frontend and canister logic to get up and running quickly. 15 | 16 |
17 | 18 | ## ✅ **Currently Supported Wallets** 19 | 20 | - **Plug Wallet** 21 | - **Internet Identity** 22 | - **NFID** 23 | - **Stoic** 24 | 25 | --- 26 | 27 | ## ⚠️ **Known Issues** 28 | 29 | - **Stoic:** A known issue with cookie storage affects Stoic logins in some browsers. This will be addressed in a future Stoic release. 30 | - **NFID:** Supports anonymous mode only for now — so canister calls work, but **payments and session delegation** are limited. This will be expanded soon. 31 | 32 | --- 33 | 34 | ## 🚀 **What’s New In Verson 1.x** 35 | 36 | - Fully modernized: Uses **latest DFINITY agent APIs** (`Agent` instead of `HttpAgent`). 37 | - Dependencies updated to **latest stable versions**. 38 | - **No more embedded frontend** — the package is framework-agnostic. 39 | - **Cleaner exports:** `UserObject` type is inline. 40 | - Local dev now supported for all providers via `host` parameter. 41 | - Removed legacy `hello` canister and embedded assets. 42 | - Designed to pair cleanly with any custom canister IDL. 43 | 44 | --- 45 | 46 | ## 📦 **Installation** 47 | 48 | ```bash 49 | npm install ic-auth 50 | ``` 51 | 52 | --- 53 | 54 | ## ✨ **Build Your Login** 55 | These steps are for creating your own simple login UI/UX using the modular functions. A more advanced design will be shown below. 56 | 57 | 58 | ### ✅ **Step 1: Import** 59 | 60 | Import the login functions you want and the universal `UserObject` type: 61 | 62 | ```ts 63 | import { PlugLogin, StoicLogin, NFIDLogin, IdentityLogin, CreateActor, UserObject } from "ic-auth"; 64 | ``` 65 | 66 | --- 67 | 68 | ### ✅ **Step 2: Handle Your Login** 69 | 70 | Create a function to trigger the login and catch the data afterwards. If you're using Plug, it requires a list of canister addresses that you plan to make calls to. This is only required for Plug so we will show that here. 71 | 72 | You will also need to define the host URL that the authentication will be looking towards for login. Again, if not using Plug you only need 73 | to toss the host as a string. 74 | 75 | ```ts 76 | const canisterID = "oyjva-2yaaa-aaaam-qbaya-cai"; 77 | const whitelist = [canisterID]; 78 | const host = "https://icp0.io" // "https://localhost:XXXX" for local dfx instances. 79 | 80 | const handleLogin = async() => { 81 | const userObject = await PlugLogin(whitelist, host); 82 | console.log(userObject); 83 | // Handle code will go here... 84 | } 85 | ``` 86 | 87 | --- 88 | 89 | ### ✅ **Step 3: Attach To Your UI** 90 | 91 | Take the login handler and attach it to a UI element of your choice. 92 | 93 | ```html 94 | 95 | ``` 96 | 97 | --- 98 | 99 | ### ✅ **Using The Login - What is a UserObject?** 100 | 101 | After a successful login you should receive the `UserObject`, which looks like this: 102 | 103 | ```ts 104 | type UserObject = { 105 | principal: string, 106 | agent: Agent | undefined, 107 | provider: string 108 | } 109 | ``` 110 | 111 | Reminder: You can import this specific type by adding `UserObject` into your imports directly from `ic-auth`. 112 | 113 | The `UserObject` can be used to create an actor for canister calls, display the user's principal address, or trigger code depending on the provider chosen. This next example shows how to create an actor using the `CreateActor` function. 114 | 115 | To create an actor you will need to pass in the canister address you wish to call, the associated canister interface description language (IDL) file, and the agent from the UserObject. You can generate the interfaces folder by running `dfx generate` after building your backend canister, then import from that folder. 116 | 117 | ```ts 118 | import { PlugLogin, StoicLogin, NFIDLogin, IdentityLogin, UserObject, CreateActor } from 'ic-auth'; 119 | import { idlFactory as YourIDL } from "./interfaces/your_canister"; 120 | 121 | const canisterID = "oyjva-2yaaa-aaaam-qbaya-cai"; 122 | const whitelist = [canisterID]; 123 | const host = "https://icp0.io"; // or your local dev host 124 | 125 | const handleLogin = async() => { 126 | const userObject = await PlugLogin(whitelist, host); 127 | console.log(userObject); 128 | const actor = await CreateActor(userObject.agent!, YourIDL, canisterID); 129 | 130 | // Handle code will go here... 131 | } 132 | ``` 133 | 134 | Now you can style the elements, add more providers, or continue on to see a more complex and full featured example. 135 | 136 | --- 137 | 138 | ## 🎛️ **Multi-Provider Login Example** 139 | 140 | This example will bring you through the steps of creating a multi-wallet supported login menu. 141 | 142 | --- 143 | 144 | ### **1️⃣ Import + Define Everything** 145 | 146 | ```ts 147 | import { PlugLogin, StoicLogin, NFIDLogin, IdentityLogin, UserObject, CreateActor } from 'ic-auth'; 148 | 149 | const canisterID = "oyjva-2yaaa-aaaam-qbaya-cai"; 150 | const whitelist = [canisterID]; 151 | const host = "https://icp0.io"; // or your local dev host 152 | ``` 153 | 154 | --- 155 | 156 | ### **2️⃣ Create a Unified Handler** 157 | 158 | ```ts 159 | const handleLogin = async (provider: string) => { 160 | let user: UserObject = { 161 | principal: "Not Connected.", 162 | agent: undefined, 163 | provider: "N/A" 164 | }; 165 | 166 | if (provider === "Plug") { 167 | user = await PlugLogin(whitelist, host); 168 | } else if (provider === "Stoic") { 169 | user = await StoicLogin(host); 170 | } else if (provider === "NFID") { 171 | user = await NFIDLogin(host); 172 | } else if (provider === "Identity") { 173 | user = await IdentityLogin(host); 174 | } 175 | 176 | console.log(user); 177 | 178 | const actor = await CreateActor(user.agent!, YourIDL, canisterID); 179 | // Interact with your canister here 180 | }; 181 | ``` 182 | 183 | --- 184 | 185 | ### **3️⃣ Attach Buttons to Your UI** 186 | 187 | ```html 188 |
189 | 190 | 191 | 192 | 193 |
194 | ``` 195 | 196 | --- 197 | 198 | ## 📚 **More Info & Resources** 199 | 200 | - 🔌 **Plug Wallet:** [plugwallet.ooo](https://plugwallet.ooo/) 201 | - 🔐 **NFID:** [nfid.one](https://nfid.one/) 202 | - 🧊 **Stoic Wallet:** [stoicwallet.com](https://stoicwallet.com/) 203 | - 🗝️ **Internet Identity:** [internetcomputer.org/internet-identity](https://internetcomputer.org/internet-identity/) 204 | - 🧩 **Internet Computer Docs (AI Powered):** [internetcomputer.org/docs/home/](https://internetcomputer.org/docs/home/) 205 | - 📌 **Mainnet Canisters:** [DFINITY Forum](https://forum.dfinity.org/t/where-can-i-find-the-canister-id-for-the-mainnet-ledger-canister/11599/2) 206 | 207 | --- 208 | 209 | **Author:** [Daniel McCoy](https://danielmccoy.us/)\ 210 | **Twitter:** [@Real](https://x.com/RealDanMcCoy)[DanMcCoy](https://x.com/RealDanMcCoy) 211 | 212 | --- 213 | 214 | Enjoy building on the Internet Computer 🚀 215 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from '@dfinity/principal'; 2 | import { Identity, HttpAgent, Agent, Actor } from '@dfinity/agent'; 3 | import { AuthClient } from '@dfinity/auth-client'; 4 | import { InterfaceFactory } from '@dfinity/candid/lib/cjs/idl'; 5 | //@ts-ignore 6 | import { StoicIdentity } from 'ic-stoic-identity'; 7 | import { ActorSubclass, ActorMethod } from '@dfinity/agent'; 8 | 9 | // Types 10 | 11 | /** 12 | * Represents an authenticated user session. 13 | */ 14 | export type UserObject = { 15 | principal: string, 16 | agent: Agent | undefined, 17 | provider: string 18 | } 19 | 20 | // Helpers 21 | 22 | const shouldFetchRootKey = (host: string): boolean => { 23 | return !(host.includes('ic0.app') || host.includes('icp0.io')); 24 | }; 25 | 26 | // A simple Stoic login flow with simplified return data. 27 | // Todo: Stoic is giving a cookies error, need to wait for a response from the stoic team. 28 | /** 29 | * Authenticate the user with the Stoic Wallet provider and return a connected UserObject. 30 | * 31 | * This flow checks if Stoic is already connected, otherwise prompts the user to connect. 32 | * If successful, it creates a new Agent tied to the identity and returns the principal and agent. 33 | * 34 | * @param host - The IC replica host to connect to (e.g., local or mainnet). 35 | * @returns A Promise resolving to a UserObject containing principal, agent, and provider info. 36 | * @throws If Stoic identity cannot be loaded or connection fails. 37 | * 38 | * @todo Stoic is currently known to throw a cookie-related error in some browsers. 39 | * This may be fixed in a later version. 40 | */ 41 | export const StoicLogin = async(host: string) : Promise => { 42 | // Checks to see if stoic is already connected or not, then prompts. 43 | let identity: Identity | null = await StoicIdentity.load(); 44 | if (!identity) { 45 | identity = await StoicIdentity.connect(); 46 | } 47 | if (!identity) { 48 | throw new Error("Stoic Identity not found or connection failed."); 49 | } 50 | const agent = await HttpAgent.create({ 51 | identity: identity, 52 | host: host, 53 | shouldFetchRootKey: shouldFetchRootKey(host) 54 | }) 55 | const principal = identity.getPrincipal().toText(); 56 | const returnObject : UserObject = { 57 | principal: principal, 58 | agent: agent, 59 | provider: "Stoic" 60 | } 61 | console.log("Connected!"); 62 | console.log(returnObject); 63 | return returnObject; 64 | } 65 | 66 | // Full featured Plug wallet login with simplified return data. 67 | /** 68 | * Logs in with Plug Wallet and returns the authenticated user's agent and information. 69 | * 70 | * @param whitelist - An array of canister IDs the app will access using Plug Wallet. 71 | * @param host - The URL of the Internet Computer replica to connect to. 72 | * Use "https://icp0.io" or "https://ic0.app" for mainnet or "http://127.0.0.1:" for local development. 73 | * @returns A Promise that resolves to a UserObject containing the agent, principal, and provider name. 74 | */ 75 | export const PlugLogin = async(whitelist: string[], host: string) : Promise => { 76 | const isConnected = await (window as any).ic.plug.isConnected() as boolean; 77 | if (isConnected) { 78 | await (window as any).ic.plug.createAgent({whitelist, host}); 79 | const agent = (window as any).ic.plug.agent as Agent; 80 | const principal = (await agent.getPrincipal()).toText(); 81 | const returnObject : UserObject = { 82 | principal: principal, 83 | agent: agent, 84 | provider: "Plug" 85 | } 86 | console.log("Connected!"); 87 | console.log(returnObject); 88 | return returnObject; 89 | } else { 90 | await (window as any).ic.plug.requestConnect({whitelist, host}); 91 | const agent = (window as any).ic.plug.agent as Agent; 92 | const principal = (await agent.getPrincipal()).toText(); 93 | const returnObject : UserObject = { 94 | principal: principal, 95 | agent: agent, 96 | provider: "Plug" 97 | } 98 | console.log("Connected!"); 99 | console.log(returnObject); 100 | return returnObject; 101 | } 102 | } 103 | 104 | // A full featured NFID login flow with modern agent and simplified return data. 105 | /** 106 | * Authenticate the user using NFID and return a connected UserObject. 107 | * 108 | * This flow uses DFINITY's AuthClient with NFID as the identity provider. 109 | * It constructs the NFID authorization URL with application name and logo, 110 | * creates a modern Agent tied to the authenticated identity, and resolves the user details. 111 | * 112 | * @param host - The IC replica host to connect to (mainnet or local). 113 | * @returns A Promise resolving to a UserObject containing principal, agent, and provider info. 114 | * @throws If creating the AuthClient or login flow fails. 115 | */ 116 | export const NFIDLogin = async (host: string): Promise => { 117 | return new Promise(async (resolve, reject) => { 118 | const appName = "wallet-testing"; 119 | const appLogo = "https://nfid.one/icons/favicon-96x96.png"; 120 | const authUrl = `https://nfid.one/authenticate/?applicationName=${appName}&applicationLogo=${appLogo}#authorize`; 121 | 122 | let userObject: UserObject = { 123 | principal: "Not Connected.", 124 | agent: undefined, 125 | provider: "N/A", 126 | }; 127 | 128 | try { 129 | const authClient = await AuthClient.create(); 130 | await authClient.login({ 131 | identityProvider: authUrl, 132 | onSuccess: async () => { 133 | const identity = authClient.getIdentity(); 134 | const agent = await HttpAgent.create({ 135 | identity, 136 | host, 137 | shouldFetchRootKey: shouldFetchRootKey(host), 138 | }); 139 | userObject = { 140 | principal: Principal.from(identity.getPrincipal()).toText(), 141 | agent, 142 | provider: "NFID", 143 | }; 144 | console.log("Connected!"); 145 | console.log(userObject); 146 | resolve(userObject); 147 | }, 148 | onError: (error) => { 149 | console.error("NFID Login Failed:", error); 150 | reject(error); 151 | }, 152 | }); 153 | } catch (error) { 154 | console.error("Error creating AuthClient:", error); 155 | reject(error); 156 | } 157 | }); 158 | }; 159 | 160 | // A fully featured Internet Identity login flow with simplified return data. 161 | /** 162 | * Authenticate the user using Internet Identity and return a connected UserObject. 163 | * 164 | * This flow uses DFINITY's AuthClient to open the Internet Identity login, 165 | * creates an Agent with the returned identity, and resolves the user details. 166 | * It handles both mainnet and local environments by conditionally fetching the root key. 167 | * 168 | * @param host - The IC replica host to connect to (mainnet or local). 169 | * @returns A Promise resolving to a UserObject containing principal, agent, and provider info. 170 | * @throws If creating the AuthClient or login flow fails. 171 | */ 172 | export const IdentityLogin = async (host: string): Promise => { 173 | return new Promise(async (resolve, reject) => { 174 | let identity: Identity; 175 | let userObject: UserObject = { 176 | principal: "Not Connected.", 177 | agent: undefined, 178 | provider: "N/A", 179 | }; 180 | 181 | try { 182 | const authClient = await AuthClient.create(); 183 | await authClient.login({ 184 | identityProvider: "https://identity.ic0.app", 185 | onSuccess: async () => { 186 | identity = authClient.getIdentity(); 187 | const agent = await HttpAgent.create({ 188 | identity: identity, 189 | host: host, 190 | shouldFetchRootKey: shouldFetchRootKey(host), 191 | }); 192 | userObject = { 193 | principal: Principal.from(identity.getPrincipal()).toText(), 194 | agent: agent, 195 | provider: "Internet Identity", 196 | }; 197 | console.log("Connected!"); 198 | console.log(userObject); 199 | resolve(userObject); 200 | }, 201 | onError: async (error: any) => { 202 | userObject = { 203 | principal: "Not Connected.", 204 | agent: undefined, 205 | provider: "N/A", 206 | }; 207 | console.log("Error Logging In"); 208 | reject(error); 209 | }, 210 | }); 211 | } catch (error) { 212 | console.error("Error creating AuthClient:", error); 213 | reject(error); 214 | } 215 | }); 216 | }; 217 | 218 | 219 | /** 220 | * Create an actor for any canister using a provided agent. 221 | * @param agent - The authenticated or anonymous Agent instance. 222 | * @param idl - The IDL factory for the target canister. 223 | * @param canisterId - The canister ID to connect to. 224 | * @returns A typed Actor subclass for the canister. 225 | */ 226 | export const CreateActor = async (agent: Agent, idl: InterfaceFactory, canisterId: string): Promise>>> => { 227 | const actor = Actor.createActor(idl, { 228 | agent, 229 | canisterId, 230 | }); 231 | return actor; 232 | }; 233 | 234 | /** 235 | * Create an anonymous agent for public queries or read-only calls. 236 | * @param host - Optional IC replica host (defaults to mainnet). 237 | * @returns An Agent instance with no identity attached. 238 | */ 239 | export const CreateAnonAgent = async (host: string): Promise => { 240 | const agent = await HttpAgent.create({ 241 | host, 242 | shouldFetchRootKey: shouldFetchRootKey(host), 243 | }); 244 | return agent; 245 | }; --------------------------------------------------------------------------------