├── .env.local ├── .env.production ├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ ├── pullrequest.yml │ └── release.yml ├── .gitignore ├── .lighthouse └── jenkins-x │ ├── pullrequest.yaml │ ├── release.yaml │ └── triggers.yaml ├── .npmrc ├── .prettierignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── charts └── sveltekit-web3auth │ ├── .helmignore │ ├── Chart.yaml │ ├── Kptfile │ ├── Makefile │ ├── README.md │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ └── ksvc.yaml │ └── values.yaml ├── docker-compose.yml ├── docs └── web3auth │ ├── 1.png │ ├── 2.png │ ├── 3.png │ └── 4.png ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── renovate.json ├── scripts └── entrypoint-sveltekit.sh ├── src ├── app.html ├── app.postcss ├── components │ ├── shared │ │ └── Header │ │ │ └── index.svelte │ └── todos │ │ └── Todo.svelte ├── config │ ├── env.ts │ └── index.ts ├── global.d.ts ├── hooks.ts ├── lib │ ├── getServerOnlyEnvVar.ts │ ├── graphQL │ │ └── urql.ts │ ├── index.ts │ ├── types.d.ts │ └── web3auth │ │ ├── LoginButton.svelte │ │ ├── LogoutButton.svelte │ │ ├── ProtectedRoute.svelte │ │ ├── RefreshTokenButton.svelte │ │ ├── Web3Auth.svelte │ │ ├── auth-api.ts │ │ ├── cookie.ts │ │ ├── hooks.ts │ │ ├── jwt.ts │ │ ├── metamask.ts │ │ ├── routes-api.ts │ │ ├── routes │ │ └── auth │ │ │ ├── login.ts │ │ │ ├── logout.ts │ │ │ ├── refresh-token.ts │ │ │ └── users │ │ │ ├── index.ts │ │ │ └── register.ts │ │ └── server-utils.ts └── routes │ ├── __layout.svelte │ ├── auth │ ├── login.ts │ ├── logout.ts │ ├── refresh-token.ts │ └── users │ │ ├── index.ts │ │ └── register.ts │ ├── index.svelte │ ├── profile │ └── index.svelte │ └── todos │ └── index.svelte ├── static └── favicon.png ├── svelte.config.js ├── tailwind.config.cjs └── tsconfig.json /.env.local: -------------------------------------------------------------------------------- 1 | # VITE_ for client side env vars 2 | VITE_WEB3AUTH_ISSUER=http://localhost:8000 3 | VITE_WEB3AUTH_CLIENT_ID=web3auth-client 4 | VITE_WEB3AUTH_CLIENT_SECRET=a114d68b22894049a7c2203a7228fdcde922a1210675427795b7bf9a0317e16d 5 | # VITE_WEB3AUTH_POST_LOGOUT_REDIRECT_URI=http://localhost:3000 // optional, just set to enable 6 | VITE_WEB3AUTH_TOKEN_REFRESH_MAX_RETRIES="5" 7 | VITE_GRAPHQL_URL=http://example-hasura.default.127.0.0.1.sslip.io/v1/graphql 8 | VITE_GRAPHQL_INTERNAL_URL=http://example-hasura.default.127.0.0.1.sslip.io/v1/graphql 9 | VITE_GRAPHQL_WS_URL=ws://example-hasura.default.127.0.0.1.sslip.io/v1/graphql 10 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # Although this file is named "production" it's used for testing production builds in a local environment 2 | # For actual production define these env vars in the chart, and get secret values using ExternalSecrets 3 | # VITE_ for client side env vars 4 | VITE_WEB3AUTH_ISSUER=http://localhost:8000 5 | VITE_WEB3AUTH_CLIENT_ID=web3auth-client 6 | # build client secret with empty string in prod mode 7 | VITE_WEB3AUTH_CLIENT_SECRET= 8 | # VITE_WEB3AUTH_POST_LOGOUT_REDIRECT_URI=http://localhost:3000 // optional, just set to enable 9 | VITE_WEB3AUTH_TOKEN_REFRESH_MAX_RETRIES="5" 10 | VITE_GRAPHQL_URL=http://example-hasura.default.127.0.0.1.sslip.io/v1/graphql 11 | VITE_GRAPHQL_INTERNAL_URL=http://example-hasura.default.127.0.0.1.sslip.io/v1/graphql 12 | VITE_GRAPHQL_WS_URL=ws://example-hasura.default.127.0.0.1.sslip.io/v1/graphql 13 | 14 | # Can only be used by server side because of no VITE_ - overwrite in kube charts with actual values 15 | WEB3AUTH_CLIENT_SECRET=a114d68b22894049a7c2203a7228fdcde922a1210675427795b7bf9a0317e16d -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .svelte-kit/** 2 | static/** 3 | build/** 4 | node_modules/** 5 | .husky/** 6 | .lighthouse/** 7 | charts/** 8 | preview/** 9 | .gitignore 10 | sveltekit-web3auth/ -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "prettier", 8 | ], 9 | plugins: ["svelte3", "@typescript-eslint"], 10 | ignorePatterns: ["*.cjs"], 11 | overrides: [{ files: ["*.svelte"], processor: "svelte3/svelte3" }], 12 | settings: { 13 | "svelte3/typescript": () => require("typescript"), 14 | }, 15 | parserOptions: { 16 | sourceType: "module", 17 | ecmaVersion: 2019, 18 | }, 19 | env: { 20 | browser: true, 21 | es2017: true, 22 | node: true, 23 | }, 24 | rules: { 25 | "@typescript-eslint/ban-ts-comment": "warn", 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /.github/workflows/pullrequest.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Pull Request 5 | 6 | on: 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [16.x, 17.x] 18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'npm' 27 | - run: npm ci 28 | - run: npm run lint --if-present 29 | - run: npm test --if-present 30 | - run: npm run build --if-present -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | - run: npm ci 20 | - run: npm run lint --if-present 21 | - run: npm run test --if-present 22 | - run: npm run package 23 | - name: Release 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | run: cd sveltekit-web3auth && npm i && npx semantic-release -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | .parcel-cache 75 | 76 | # Next.js build output 77 | .next 78 | out 79 | 80 | # Nuxt.js build / generate output 81 | .nuxt 82 | dist 83 | 84 | # Gatsby files 85 | .cache/ 86 | # Comment in the public line in if your project uses Gatsby and not Next.js 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # public 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # Serverless directories 94 | .serverless/ 95 | 96 | # FuseBox cache 97 | .fusebox/ 98 | 99 | # DynamoDB Local files 100 | .dynamodb/ 101 | 102 | # TernJS port file 103 | .tern-port 104 | 105 | # Stores VSCode versions used for testing VSCode extensions 106 | .vscode-test 107 | 108 | # yarn v2 109 | .yarn/cache 110 | .yarn/unplugged 111 | .yarn/build-state.yml 112 | .yarn/install-state.gz 113 | .pnp.* 114 | 115 | # sveltekit 116 | .svelte-kit/ 117 | build/ 118 | storybook-static/ 119 | sveltekit-web3auth/ 120 | 121 | .env.* 122 | !.env.local 123 | !.env.production -------------------------------------------------------------------------------- /.lighthouse/jenkins-x/pullrequest.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1beta1 2 | kind: PipelineRun 3 | metadata: 4 | creationTimestamp: null 5 | name: pullrequest 6 | spec: 7 | pipelineSpec: 8 | tasks: 9 | - name: from-build-pack 10 | resources: {} 11 | taskSpec: 12 | metadata: 13 | annotations: 14 | sidecar.istio.io/inject: "false" 15 | stepTemplate: 16 | image: uses:jenkins-x/jx3-pipeline-catalog/tasks/docker-helm/pullrequest.yaml@versionStream 17 | name: "" 18 | resources: 19 | requests: 20 | cpu: 400m 21 | memory: 512Mi 22 | workingDir: /workspace/source 23 | steps: 24 | - image: uses:jenkins-x/jx3-pipeline-catalog/tasks/git-clone/git-clone-pr.yaml@versionStream 25 | name: "" 26 | resources: {} 27 | - name: jx-variables 28 | resources: {} 29 | - name: build-container-build 30 | resources: 31 | requests: 32 | cpu: 1500m 33 | memory: 1500Mi 34 | env: 35 | - name: KANIKO_FLAGS 36 | value: --snapshotMode=redo 37 | - image: ghcr.io/jenkins-x-plugins/jx-preview:0.0.192 38 | name: promote-jx-preview 39 | resources: {} 40 | script: | 41 | #!/usr/bin/env sh 42 | source .jx/variables.sh 43 | jx preview create --no-comment 44 | - image: ghcr.io/jenkins-x/jx-boot:3.2.205 45 | name: comment-with-link 46 | resources: {} 47 | script: | 48 | #!/usr/bin/env sh 49 | source .jx/variables.sh 50 | export SVELTEKIT_WEB3AUTH_KSVC_URL=$(kubectl get ksvc -n $PREVIEW_NAMESPACE $REPO_NAME -o 'jsonpath={.status.url}') 51 | jx gitops pr comment \ 52 | -c "🔥 PR Preview Environment is starting - if this is a new preview environment then SSL certs may take a few minutes to be provisioned... [sveltekit-web3auth]($SVELTEKIT_WEB3AUTH_KSVC_URL)" \ 53 | --repo $REPO_OWNER/$REPO_NAME \ 54 | --pr $PULL_NUMBER 55 | podTemplate: {} 56 | serviceAccountName: tekton-bot 57 | timeout: 12h0m0s 58 | status: {} 59 | -------------------------------------------------------------------------------- /.lighthouse/jenkins-x/release.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1beta1 2 | kind: PipelineRun 3 | metadata: 4 | creationTimestamp: null 5 | name: release 6 | spec: 7 | pipelineSpec: 8 | tasks: 9 | - name: from-build-pack 10 | resources: {} 11 | taskSpec: 12 | metadata: 13 | annotations: 14 | sidecar.istio.io/inject: "false" 15 | stepTemplate: 16 | env: 17 | - name: NPM_CONFIG_USERCONFIG 18 | value: /tekton/home/npm/.npmrc 19 | image: uses:jenkins-x/jx3-pipeline-catalog/tasks/javascript/release.yaml@versionStream 20 | name: "" 21 | resources: 22 | requests: 23 | cpu: 400m 24 | memory: 512Mi 25 | volumeMounts: 26 | - mountPath: /tekton/home/npm 27 | name: npmrc 28 | workingDir: /workspace/source 29 | steps: 30 | - image: uses:jenkins-x/jx3-pipeline-catalog/tasks/git-clone/git-clone.yaml@versionStream 31 | name: "" 32 | resources: {} 33 | - name: next-version 34 | resources: {} 35 | - name: jx-variables 36 | resources: {} 37 | - name: build-container-build 38 | resources: 39 | requests: 40 | cpu: 1500m 41 | memory: 1500Mi 42 | env: 43 | - name: KANIKO_FLAGS 44 | value: --snapshotMode=redo 45 | - name: promote-changelog 46 | resources: {} 47 | - name: promote-helm-release 48 | resources: {} 49 | - name: promote-jx-promote 50 | resources: {} 51 | volumes: 52 | - name: npmrc 53 | secret: 54 | optional: true 55 | secretName: npmrc 56 | podTemplate: {} 57 | serviceAccountName: tekton-bot 58 | timeout: 12h0m0s 59 | status: {} 60 | -------------------------------------------------------------------------------- /.lighthouse/jenkins-x/triggers.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.lighthouse.jenkins-x.io/v1alpha1 2 | kind: TriggerConfig 3 | spec: 4 | presubmits: 5 | - name: pr 6 | context: "pr" 7 | always_run: true 8 | optional: false 9 | source: "pullrequest.yaml" 10 | postsubmits: 11 | - name: release 12 | context: "release" 13 | source: "release.yaml" 14 | ignore_changes: '^(\.lighthouse\/jenkins-x\/pullrequest\.yaml)|(preview\/.+)$' 15 | branches: 16 | - ^main$ 17 | - ^master$ 18 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .svelte-kit/** 2 | static/** 3 | build/** 4 | node_modules/** 5 | .husky/** 6 | .lighthouse/** 7 | charts/** 8 | preview/** 9 | src/lib/env.js 10 | coverage/** 11 | src/stories/Introduction.stories.mdx 12 | .storybook/package.json 13 | docker-compose.yml 14 | .github/** 15 | storybook-static/** 16 | sveltekit-web3auth/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:17.2.0-alpine3.13 AS build 2 | 3 | WORKDIR /build 4 | 5 | COPY package* ./ 6 | RUN npm ci 7 | 8 | COPY *.js *.cjs .*ignore .*rc ./ 9 | COPY static/ static/ 10 | COPY src/ src/ 11 | # COPY __tests__/ __tests__/ 12 | # COPY jest.json jest.json 13 | 14 | COPY scripts scripts 15 | 16 | EXPOSE 3000 17 | 18 | ENTRYPOINT [ "ash", "./scripts/entrypoint-sveltekit.sh" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Patrick Lee Scott 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | onboard: install 2 | 3 | open: 4 | code . 5 | 6 | install: 7 | npm ci 8 | 9 | dev: 10 | npm run dev 11 | 12 | prod: 13 | npm run build 14 | npx dotenv -e .env.production -- node ./build/index.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sveltekit + web3auth 2 | 3 | This project aims to integrate web3auth via MetaMask with a JWT Issuing auth server from a confidential client for use with APIs in Sveltekit. Once login is complete, Navigation to protected pages of app don't require a request to Authorization Server. Sveltekit hooks take care of : 4 | 5 | [x] Silent Refresh Workflow 6 | [x] Validating the client accessToken validity 7 | [x] Renewing the token in case of token expiry 8 | [x] Offline Auth server error handling 9 | [x] Setting valid user information ( accessToken, refreshToken, userid etc. ) in form of cookies 10 | [x] Populating session variable with user information 11 | 12 | When the client side kicks in, it: 13 | 14 | [x] Checks for user and Auth server information in session variable 15 | [x] In case, no user is found or some error has occured on server-side, populate AuthStore with proper messages 16 | [x] Provides Login, Logout functionality 17 | [x] Initiates authorization flow, in case of protected component via Sveletkit Load method. 18 | [x] Logout in one browser tab initiates automatic logout from all tabs. 19 | [x] Prompt on all browser tabs and Page reloading on User Login. 20 | 21 | Goal is complete JWT Implementation based on [Hasura Blog on BEST Practices for JWT AUTH](https://hasura.io/blog/best-practices-of-using-jwt-with-graphql/) in context of meta mask login and challenge/signature auth flow. 22 | 23 | More useful reading: 24 | 25 | - https://github.com/vnovick/graphql-jwt-tutorial/ 26 | - https://www.toptal.com/ethereum/one-click-login-flows-a-metamask-tutorial 27 | - https://github.com/amaurym/login-with-metamask-demo 28 | 29 | ### Npm Package link 30 | 31 | https://www.npmjs.com/package/sveltekit-web3auth 32 | 33 | # Usage 34 | 35 | ## Template 36 | 37 | The easiest way to get started is with the template: https://github.com/CloudNativeEntrepreneur/sveltekit-web3auth-template 38 | 39 | ## Installation 40 | 41 | 42 | npm i sveltekit-web3auth --save-dev 43 | 44 | 45 | ## Server 46 | 47 | You can use this server: [CloudNativeEntrepreneur/web3-auth-service](https://github.com/CloudNativeEntrepreneur/web3-auth-service) 48 | 49 | ## Configuration 50 | 51 | Create an .env file in project root with following content 52 | 53 | ```ts 54 | VITE_WEB3_AUTH_ISSUER="http://localhost:8000" 55 | VITE_WEB3_AUTH_CLIENT_ID="local-public" 56 | VITE_WEB3_AUTH_CLIENT_SECRET="1439e34f-343e-4f71-bbc7-cc602dced84a" 57 | // VITE_WEB3_AUTH_POST_LOGOUT_REDIRECT_URI="http://localhost:3000" // optional, just set to enable 58 | VITE_WEB3_AUTH_TOKEN_REFRESH_MAX_RETRIES="5" 59 | VITE_GRAPHQL_URL=http://hasura.default.127.0.0.1.sslip.io/v1/graphql 60 | VITE_GRAPHQL_INTERNAL_URL=http://hasura.default.127.0.0.1.sslip.io/v1/graphql 61 | VITE_GRAPHQL_WS_URL=ws://hasura.default.127.0.0.1.sslip.io/v1/graphql 62 | ``` 63 | 64 | ### Inside your src/global.d.ts 65 | 66 | ```ts 67 | interface ImportMetaEnv { 68 | VITE_WEB3AUTH_ISSUER: string; 69 | VITE_WEB3AUTH_CLIENT_ID: string; 70 | VITE_WEB3AUTH_CLIENT_SECRET: string; 71 | VITE_WEB3AUTH_POST_LOGOUT_REDIRECT_URI?: string; 72 | VITE_WEB3AUTH_TOKEN_REFRESH_MAX_RETRIES: number; 73 | VITE_GRAPHQL_URL: string; 74 | VITE_GRAPHQL_INTERNAL_URL: string; 75 | VITE_GRAPHQL_WS_URL: string; 76 | } 77 | ``` 78 | 79 | ## Auth Endpoints 80 | 81 | SvelteKit only includes the `$lib` folder in published packages, so you'll need to set up the needed routes to support the confidential authentication flow in your own project. 82 | 83 | From [the source repo of sveltekit-web3auth](https://github.com/CloudNativeEntrepreneur/sveltekit-web3auth), copy the `src/routes/auth` folder into your own SvelteKit project. Also copy the `src/config` folder, the `src/hooks.ts` file, and `src/routes/__layout.svelte` into the same spots in your own project. 84 | 85 | Replace imports of `$lib` with `sveltekit-web3auth`. 86 | 87 | You may also optionally copy in the `routes/graphql` and `routes/profile` 88 | 89 | Feel free to customize them after this point. 90 | 91 | ### Use these stores for auth information 92 | 93 | ```html 94 | 106 | 107 | {#if $isAuthenticated} 108 |
User is authenticated
109 | {:else} 110 | Login 111 | {/if} 112 |
113 | ``` 114 | 115 | ### For protected routes 116 | 117 | ```html 118 | 121 | 122 | 123 |
126 | This is a protected page 127 | 128 | Logout 129 |
130 |
131 | ``` 132 | 133 | # Application Screenshots 134 | 135 | ### Login / Index page 136 | 137 | ![Login Page](https://github.com/CloudNativeEntrepreneur/sveltekit-web3auth/blob/main/docs/web3auth/1.png?raw=true) 138 | 139 | ### Once user clicks login, Redirection to Auth server 140 | 141 | ![Metamask Auth](https://github.com/CloudNativeEntrepreneur/sveltekit-web3auth/blob/main/docs/web3auth/2.png?raw=true) 142 | 143 | ### Auth Complete - client hydrated with accessToken 144 | 145 | ![Index page with JWT](https://github.com/CloudNativeEntrepreneur/sveltekit-web3auth/blob/main/docs/web3auth/3.png?raw=true) 146 | 147 | ### Protected Page and Session variables with user info 148 | 149 | ![Index page with JWT](https://github.com/CloudNativeEntrepreneur/sveltekit-web3auth/blob/main/docs/web3auth/4.png?raw=true) 150 | 151 | ## Developing 152 | 153 | Once you've created a project and installed dependencies with `npm ci`, start a development server: 154 | 155 | ```bash 156 | npm run dev 157 | ``` 158 | 159 | ## Building 160 | 161 | When building for production, sveltekit will use `.env.production` values. 162 | 163 | In production mode it's important to build with a blank `VITE_WEB3AUTH_CLIENT_SECRET` so it is not accessible on the client side. Instead, `WEB3AUTH_CLIENT_SECRET` will be accessible to the process at runtime during a run of a production build. 164 | 165 | ```bash 166 | npm run build 167 | ``` 168 | 169 | # FAQ 170 | 171 | ## YOUR SECRET IS EXPOSED 172 | 173 | Yes, I know. 174 | 175 | It'll only work in the local development cluster - this is part of an example that contains several moving parts, so I just generated some random secrets where they were needed and preconfigured things accordingly so you can just run it locally and everything will work. Couldn't go without secrets as part of that example is an authentication server and it's JWT integration with Hasura. 176 | 177 | Don't use these proconfigured values in production. I typically use ExternalSecrets in prod. 178 | 179 | Additionally, because Sveltekit generates code for the client and server (at least in this example), we need to be careful about how to build and configure prod, and I wanted to provide an example of that too. 180 | 181 | I generally deploy things to Kubernetes, which is what the charts folder is for, and in a real chart I'd use ExternalSecrets to provide those values. 182 | -------------------------------------------------------------------------------- /charts/sveltekit-web3auth/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /charts/sveltekit-web3auth/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | description: A Helm chart for sveltekit-web3auth 3 | icon: https://raw.githubusercontent.com/sveltejs/svelte/29052aba7d0b78316d3a52aef1d7ddd54fe6ca84/site/static/images/svelte-android-chrome-512.png 4 | name: sveltekit-web3auth 5 | version: 0.0.1 6 | -------------------------------------------------------------------------------- /charts/sveltekit-web3auth/Kptfile: -------------------------------------------------------------------------------- 1 | apiVersion: kpt.dev/v1alpha1 2 | kind: Kptfile 3 | metadata: 4 | name: charts 5 | upstream: 6 | type: git 7 | git: 8 | commit: 86fd41fbf45c12cf799c9dcf587a3d2c73dcaf75 9 | repo: https://github.com/jenkins-x/jx3-pipeline-catalog 10 | directory: /helm/charts 11 | ref: master 12 | -------------------------------------------------------------------------------- /charts/sveltekit-web3auth/Makefile: -------------------------------------------------------------------------------- 1 | CHART_REPO := http://jenkins-x-chartmuseum:8080 2 | CURRENT=$(pwd) 3 | NAME := sveltekit-web3auth 4 | OS := $(shell uname) 5 | RELEASE_VERSION := $(shell cat ../../VERSION) 6 | 7 | build: clean 8 | rm -rf requirements.lock 9 | helm dependency build 10 | helm lint 11 | 12 | install: clean build 13 | helm install . --name ${NAME} 14 | 15 | upgrade: clean build 16 | helm upgrade ${NAME} . 17 | 18 | delete: 19 | helm delete --purge ${NAME} 20 | 21 | clean: 22 | rm -rf charts 23 | rm -rf ${NAME}*.tgz 24 | 25 | release: clean 26 | helm dependency build 27 | helm lint 28 | helm init --client-only 29 | helm package . 30 | curl --fail -u $(CHARTMUSEUM_CREDS_USR):$(CHARTMUSEUM_CREDS_PSW) --data-binary "@$(NAME)-$(shell sed -n 's/^version: //p' Chart.yaml).tgz" $(CHART_REPO)/api/charts 31 | rm -rf ${NAME}*.tgz% 32 | 33 | tag: 34 | ifeq ($(OS),Darwin) 35 | sed -i "" -e "s/version:.*/version: $(RELEASE_VERSION)/" Chart.yaml 36 | sed -i "" -e "s/tag:.*/tag: $(RELEASE_VERSION)/" values.yaml 37 | else ifeq ($(OS),Linux) 38 | sed -i -e "s/version:.*/version: $(RELEASE_VERSION)/" Chart.yaml 39 | sed -i -e "s|repository:.*|repository: $(DOCKER_REGISTRY)\/cldntventr\/sveltekit-web3auth|" values.yaml 40 | sed -i -e "s/tag:.*/tag: $(RELEASE_VERSION)/" values.yaml 41 | else 42 | echo "platfrom $(OS) not supported to release from" 43 | exit -1 44 | endif 45 | git add --all 46 | git commit -m "release $(RELEASE_VERSION)" --allow-empty # if first release then no verion update is performed 47 | git tag -fa v$(RELEASE_VERSION) -m "Release version $(RELEASE_VERSION)" 48 | git push origin v$(RELEASE_VERSION) 49 | -------------------------------------------------------------------------------- /charts/sveltekit-web3auth/README.md: -------------------------------------------------------------------------------- 1 | # sveltekit-web3auth -------------------------------------------------------------------------------- /charts/sveltekit-web3auth/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 2 | Get the application URL by running these commands: 3 | 4 | kubectl get ingress {{ template "fullname" . }} 5 | -------------------------------------------------------------------------------- /charts/sveltekit-web3auth/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | */}} 13 | {{- define "fullname" -}} 14 | {{- $name := default .Chart.Name .Values.nameOverride -}} 15 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /charts/sveltekit-web3auth/templates/ksvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: serving.knative.dev/v1 2 | kind: Service 3 | metadata: 4 | name: {{ .Values.service.name }} 5 | labels: 6 | chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" 7 | annotations: 8 | {{- if .Values.knative.subdomain }} 9 | custom-hostname: {{ .Values.knative.subdomain }} 10 | {{- end }} 11 | spec: 12 | template: 13 | metadata: 14 | annotations: 15 | autoscaling.knative.dev/minScale: "1" 16 | spec: 17 | containers: 18 | - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 19 | ports: 20 | - containerPort: {{ .Values.service.internalPort }} 21 | env: 22 | - name: VITE_GRAPHQL_URL 23 | value: https://example-hasura.{{ .Release.Namespace }}.jx.cloudnativeentrepreneur.dev/v1/graphql 24 | - name: VITE_GRAPHQL_INTERNAL_URL 25 | value: https://example-hasura.{{ .Release.Namespace }}.jx.cloudnativeentrepreneur.dev/v1/graphql 26 | - name: VITE_GRAPHQL_WS_URL 27 | value: wss://example-hasura.{{ .Release.Namespace }}.jx.cloudnativeentrepreneur.dev/v1/graphql 28 | {{- range $pkey, $pval := .Values.env }} 29 | - name: {{ $pkey }} 30 | value: {{ quote $pval }} 31 | {{- end }} 32 | livenessProbe: 33 | httpGet: 34 | path: {{ .Values.livenessProbe.probePath | default .Values.probePath }} 35 | initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} 36 | periodSeconds: {{ .Values.livenessProbe.periodSeconds }} 37 | successThreshold: {{ .Values.livenessProbe.successThreshold }} 38 | timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} 39 | readinessProbe: 40 | httpGet: 41 | path: {{ .Values.livenessProbe.probePath | default .Values.probePath }} 42 | initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} 43 | periodSeconds: {{ .Values.readinessProbe.periodSeconds }} 44 | successThreshold: {{ .Values.readinessProbe.successThreshold }} 45 | timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} 46 | resources: 47 | {{ toYaml .Values.resources | indent 10 }} 48 | -------------------------------------------------------------------------------- /charts/sveltekit-web3auth/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for node projects. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | image: 5 | repository: draft 6 | tag: dev 7 | pullPolicy: IfNotPresent 8 | 9 | # define environment variables here as a map of key: value 10 | env: 11 | 12 | service: 13 | name: sveltekit-web3auth 14 | internalPort: 3000 15 | 16 | knative: {} 17 | 18 | probePath: / 19 | livenessProbe: 20 | initialDelaySeconds: 90 21 | periodSeconds: 10 22 | successThreshold: 1 23 | timeoutSeconds: 1 24 | readinessProbe: 25 | initialDelaySeconds: 90 26 | failureThreshold: 1 27 | periodSeconds: 10 28 | successThreshold: 1 29 | timeoutSeconds: 1 30 | 31 | resources: 32 | limits: 33 | cpu: '1' 34 | memory: 1024Mi 35 | requests: 36 | cpu: 50m 37 | memory: 256Mi 38 | 39 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | sveltekit-web3auth: 6 | image: sveltekit-web3auth 7 | build: 8 | context: . 9 | env_file: 10 | - .env 11 | ports: 12 | - 3000:3000 13 | -------------------------------------------------------------------------------- /docs/web3auth/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeEntrepreneur/sveltekit-web3auth/d47ca2f3d16aa7015382e71d24e8e641d5b2e395/docs/web3auth/1.png -------------------------------------------------------------------------------- /docs/web3auth/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeEntrepreneur/sveltekit-web3auth/d47ca2f3d16aa7015382e71d24e8e641d5b2e395/docs/web3auth/2.png -------------------------------------------------------------------------------- /docs/web3auth/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeEntrepreneur/sveltekit-web3auth/d47ca2f3d16aa7015382e71d24e8e641d5b2e395/docs/web3auth/3.png -------------------------------------------------------------------------------- /docs/web3auth/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeEntrepreneur/sveltekit-web3auth/d47ca2f3d16aa7015382e71d24e8e641d5b2e395/docs/web3auth/4.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltekit-web3auth", 3 | "version": "0.0.0-development", 4 | "scripts": { 5 | "dev": "DEBUG=sveltekit-web3auth:* svelte-kit dev", 6 | "build": "svelte-kit build", 7 | "package": "svelte-kit package", 8 | "preview": "svelte-kit preview", 9 | "check": "svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --check --plugin-search-dir=. . && eslint .", 12 | "format": "prettier --write --plugin-search-dir=. .", 13 | "semantic-release": "semantic-release" 14 | }, 15 | "devDependencies": { 16 | "@sveltejs/adapter-node": "1.0.0-next.70", 17 | "@sveltejs/kit": "1.0.0-next.287", 18 | "@typescript-eslint/eslint-plugin": "5.21.0", 19 | "@urql/svelte": "1.3.3", 20 | "autoprefixer": "10.4.2", 21 | "cssnano": "5.1.7", 22 | "debug": "4.3.3", 23 | "dotenv-cli": "5.0.0", 24 | "eslint": "8.10.0", 25 | "eslint-config-prettier": "8.5.0", 26 | "eslint-plugin-svelte3": "3.4.1", 27 | "graphql-ws": "5.6.2", 28 | "jwt-decode": "3.1.2", 29 | "postcss": "8.4.12", 30 | "postcss-import": "14.0.2", 31 | "postcss-load-config": "3.1.3", 32 | "postcss-preset-env": "7.4.4", 33 | "prettier": "2.5.1", 34 | "prettier-plugin-svelte": "2.6.0", 35 | "semantic-release": "19.0.2", 36 | "svelte": "3.46.4", 37 | "svelte-check": "2.4.5", 38 | "svelte-preprocess": "4.10.4", 39 | "svelte2tsx": "0.5.5", 40 | "tailwindcss": "3.0.23", 41 | "typescript": "4.6.4", 42 | "ws": "8.5.0" 43 | }, 44 | "peerDependencies": { 45 | "@urql/svelte": "1.x", 46 | "debug": "4.x", 47 | "graphql-ws": "5.x", 48 | "jwt-decode": "3.x", 49 | "ws": "8.x" 50 | }, 51 | "license": "MIT", 52 | "keywords": [ 53 | "sveltekit-web3auth", 54 | "auth", 55 | "authentication", 56 | "jwt", 57 | "nodejs", 58 | "web3", 59 | "ethereum", 60 | "sveltekit" 61 | ], 62 | "author": "Patrick Lee Scott", 63 | "repository": "https://github.com/CloudNativeEntrepreneur/sveltekit-web3auth.git", 64 | "type": "module" 65 | } 66 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const postcssImport = require("postcss-import"); 2 | const tailwindcss = require("tailwindcss"); 3 | const nesting = require("tailwindcss/nesting"); 4 | const autoprefixer = require("autoprefixer"); 5 | const cssnano = require("cssnano"); 6 | const presetEnv = require("postcss-preset-env")({ 7 | stage: 1, 8 | features: { 9 | // tailwind plugin handles 10 | "nesting-rules": false, 11 | }, 12 | }); 13 | 14 | const mode = process.env.NODE_ENV; 15 | const dev = mode === "development"; 16 | 17 | const config = { 18 | plugins: [ 19 | postcssImport, 20 | nesting, 21 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind, 22 | tailwindcss, 23 | //But others, like autoprefixer, need to run after, 24 | autoprefixer, 25 | presetEnv, 26 | !dev && 27 | cssnano({ 28 | preset: "default", 29 | }), 30 | ], 31 | }; 32 | 33 | module.exports = config; 34 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"] 3 | } 4 | -------------------------------------------------------------------------------- /scripts/entrypoint-sveltekit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm run build 4 | 5 | npm prune --production 6 | rm -rf ./src 7 | 8 | node ./build/index.js -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %svelte.head% 8 | 9 | 10 |
%svelte.body%
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/app.postcss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | /* Write your global styles here, in PostCSS syntax */ 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | @layer components { 7 | .btn { 8 | @apply px-4 py-2 rounded-md shadow-md font-semibold m-2 transition-colors duration-300 ease-in-out; 9 | } 10 | .btn-primary { 11 | @apply bg-gray-200 text-gray-800 hover:bg-gray-700 hover:text-gray-100; 12 | } 13 | 14 | .nav-link { 15 | border-top-color: transparent; 16 | @apply border-t-4 border-b-4 border-opacity-0 flex flex-col justify-center items-center h-full hover:bg-gray-600 hover:text-gray-100; 17 | } 18 | .nav-active { 19 | @apply border-b-green-400 border-opacity-100; 20 | } 21 | .nav-link > a { 22 | @apply px-4 py-2 h-full flex flex-col justify-center items-center; 23 | } 24 | 25 | .h-screen-minus-navbar { 26 | height: calc(100vh - 48px); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/shared/Header/index.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 39 | -------------------------------------------------------------------------------- /src/components/todos/Todo.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
25 |

26 | {todo.todo} 27 |

28 | {#if todo.completed} 29 | 33 | {:else} 34 | 38 | {/if} 39 | 43 |
44 | -------------------------------------------------------------------------------- /src/config/env.ts: -------------------------------------------------------------------------------- 1 | export const VITE_GRAPHQL_URL = import.meta.env.VITE_GRAPHQL_URL; 2 | 3 | export const VITE_GRAPHQL_INTERNAL_URL = import.meta.env 4 | .VITE_GRAPHQL_INTERNAL_URL; 5 | 6 | export const VITE_GRAPHQL_WS_URL = import.meta.env.VITE_GRAPHQL_WS_URL; 7 | 8 | export const VITE_WEB3AUTH_ISSUER: string = import.meta.env 9 | .VITE_WEB3AUTH_ISSUER; 10 | 11 | export const VITE_WEB3AUTH_CLIENT_ID: string = import.meta.env 12 | .VITE_WEB3AUTH_CLIENT_ID; 13 | 14 | export const VITE_WEB3AUTH_CLIENT_SECRET: string = import.meta.env 15 | .VITE_WEB3AUTH_CLIENT_SECRET; 16 | 17 | export const VITE_WEB3AUTH_POST_LOGOUT_REDIRECT_URI: string = import.meta.env 18 | .VITE_WEB3AUTH_POST_LOGOUT_REDIRECT_URI; 19 | 20 | export const VITE_WEB3AUTH_TOKEN_REFRESH_MAX_RETRIES: number = import.meta.env 21 | .VITE_WEB3AUTH_TOKEN_REFRESH_MAX_RETRIES; 22 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | VITE_GRAPHQL_URL, 3 | VITE_GRAPHQL_INTERNAL_URL, 4 | VITE_GRAPHQL_WS_URL, 5 | VITE_WEB3AUTH_ISSUER, 6 | VITE_WEB3AUTH_CLIENT_ID, 7 | VITE_WEB3AUTH_CLIENT_SECRET, 8 | VITE_WEB3AUTH_POST_LOGOUT_REDIRECT_URI, 9 | VITE_WEB3AUTH_TOKEN_REFRESH_MAX_RETRIES, 10 | } from "./env"; 11 | 12 | export const config = { 13 | web3auth: { 14 | issuer: VITE_WEB3AUTH_ISSUER, 15 | clientId: VITE_WEB3AUTH_CLIENT_ID, 16 | clientSecret: VITE_WEB3AUTH_CLIENT_SECRET, 17 | postLogoutRedirectUri: VITE_WEB3AUTH_POST_LOGOUT_REDIRECT_URI, 18 | refreshTokenMaxRetries: VITE_WEB3AUTH_TOKEN_REFRESH_MAX_RETRIES, 19 | }, 20 | graphql: { 21 | http: VITE_GRAPHQL_URL, 22 | httpInternal: VITE_GRAPHQL_INTERNAL_URL, 23 | ws: VITE_GRAPHQL_WS_URL, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface ImportMetaEnv { 3 | VITE_WEB3AUTH_ISSUER: string; 4 | VITE_WEB3AUTH_CLIENT_ID: string; 5 | VITE_WEB3AUTH_CLIENT_SECRET: string; 6 | VITE_WEB3AUTH_POST_LOGOUT_REDIRECT_URI?: string; 7 | VITE_WEB3AUTH_TOKEN_REFRESH_MAX_RETRIES: number; 8 | VITE_GRAPHQL_URL: string; 9 | VITE_GRAPHQL_INTERNAL_URL: string; 10 | VITE_GRAPHQL_WS_URL: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import type { Handle, GetSession } from "@sveltejs/kit"; 2 | import { 3 | userDetailsGenerator, 4 | getUserSession, 5 | getServerOnlyEnvVar, 6 | } from "$lib"; 7 | import type { Locals } from "$lib/types"; 8 | import type { RequestEvent } from "@sveltejs/kit/types/hooks"; 9 | import { config } from "./config"; 10 | import debug from "debug"; 11 | 12 | const log = debug("sveltekit-web3auth:hooks"); 13 | 14 | const issuer = config.web3auth.issuer; 15 | const clientId = config.web3auth.clientId; 16 | const clientSecret = 17 | getServerOnlyEnvVar(process, "WEB3AUTH_CLIENT_SECRET") || 18 | config.web3auth.clientSecret; 19 | const refreshTokenMaxRetries = config.web3auth.refreshTokenMaxRetries; 20 | 21 | // https://kit.svelte.dev/docs#hooks-handle 22 | export const handle: Handle = async ({ event, resolve }) => { 23 | log("handle", event.request.url); 24 | 25 | // Initialization part 26 | const userGen = userDetailsGenerator(event); 27 | 28 | const { value, done } = await userGen.next(); 29 | 30 | if (done) { 31 | const response = value; 32 | return response; 33 | } 34 | 35 | // Set Cookie attributes 36 | event.locals.cookieAttributes = "Path=/; HttpOnly;"; 37 | 38 | // response is the page sveltekit route that was rendered, we're 39 | // intercepting it and adding headers on the way out 40 | const response = await resolve(event); 41 | 42 | if (response?.status === 404) { 43 | return response; 44 | } 45 | 46 | const body = await response.text(); 47 | 48 | const authResponse = (await userGen.next(event)).value; 49 | const { Location } = authResponse.headers; 50 | 51 | // SSR Redirection 52 | if (authResponse.status === 302 && Location) { 53 | const redirectResponse = { 54 | ...response, 55 | status: authResponse.status, 56 | headers: { 57 | "content-type": response.headers.get("content-type"), 58 | etag: response.headers.get("etag"), 59 | "permissions-policy": response.headers.get("permissions-policy"), 60 | Location, 61 | }, 62 | }; 63 | 64 | return new Response(body, redirectResponse); 65 | } 66 | 67 | if (authResponse?.headers) { 68 | let authedResponseBase; 69 | 70 | if (authResponse?.headers?.userid) { 71 | authedResponseBase = { 72 | status: response.status, 73 | statusText: response.statusText, 74 | headers: { 75 | user: authResponse.headers.user, 76 | userid: authResponse.headers.userid, 77 | accesstoken: authResponse.headers.accesstoken, 78 | refreshtoken: authResponse.headers.refreshtoken, 79 | "set-cookie": authResponse.headers["set-cookie"], 80 | "content-type": response.headers.get("content-type"), 81 | etag: response.headers.get("etag"), 82 | "permissions-policy": response.headers.get("permissions-policy"), 83 | }, 84 | }; 85 | } else if (authResponse.headers["set-cookie"]) { 86 | authedResponseBase = { 87 | status: response.status, 88 | statusText: response.statusText, 89 | headers: { 90 | "set-cookie": authResponse.headers["set-cookie"], 91 | "content-type": response.headers.get("content-type"), 92 | etag: response.headers.get("etag"), 93 | "permissions-policy": response.headers.get("permissions-policy"), 94 | }, 95 | }; 96 | } 97 | return new Response(body, authedResponseBase); 98 | } 99 | 100 | return new Response(body, response); 101 | }; 102 | 103 | /** @type {import('@sveltejs/kit').GetSession} */ 104 | export const getSession: GetSession = async (event: RequestEvent) => { 105 | log("getting user session..."); 106 | 107 | const userSession = await getUserSession( 108 | event, 109 | issuer, 110 | clientId, 111 | clientSecret, 112 | refreshTokenMaxRetries 113 | ); 114 | 115 | return userSession; 116 | }; 117 | -------------------------------------------------------------------------------- /src/lib/getServerOnlyEnvVar.ts: -------------------------------------------------------------------------------- 1 | export const getServerOnlyEnvVar = (config: any, key) => { 2 | const { env } = config; 3 | return env[key]; 4 | }; 5 | -------------------------------------------------------------------------------- /src/lib/graphQL/urql.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createClient, 3 | subscriptionExchange, 4 | ssrExchange, 5 | dedupExchange, 6 | cacheExchange, 7 | fetchExchange, 8 | } from "@urql/svelte"; 9 | import { accessToken } from "../web3auth/Web3Auth.svelte"; 10 | import { browser } from "$app/env"; 11 | import debug from "debug"; 12 | import { createClient as createGQLClient } from "graphql-ws"; 13 | 14 | const log = debug("sveltekit-web3auth:lib/graphQL/urql"); 15 | 16 | const graphQLClients = []; 17 | 18 | type AuthHeaders = { 19 | Authorization?: string | undefined; 20 | }; 21 | 22 | let currentAccessToken; 23 | const authHeaders: AuthHeaders = {}; 24 | let currentFetchOptions; 25 | const fetchOptions = () => { 26 | return currentFetchOptions; 27 | }; 28 | let restartRequired = false; 29 | 30 | accessToken.subscribe((value) => { 31 | currentAccessToken = value; 32 | if (currentAccessToken) { 33 | authHeaders.Authorization = `Bearer ${currentAccessToken}`; 34 | } 35 | currentFetchOptions = { 36 | headers: { 37 | ...authHeaders, 38 | }, 39 | }; 40 | restartRequired = true; 41 | }); 42 | 43 | export const graphQLClient = (options: { 44 | id; 45 | session; 46 | graphql: { 47 | ws: string; 48 | http: string; 49 | httpInternal?: string; 50 | }; 51 | fetch; 52 | ws; 53 | clientSideCacheMode?: string; 54 | serverSideCacheMode?: string; 55 | }) => { 56 | const { id, session, graphql, fetch, ws } = options; 57 | let { clientSideCacheMode, serverSideCacheMode } = options; 58 | const isServerSide = !browser; 59 | 60 | clientSideCacheMode = clientSideCacheMode || "network-only"; 61 | serverSideCacheMode = serverSideCacheMode || "network-only"; 62 | log("gql client init/restart", { 63 | restartRequired, 64 | id, 65 | clientSideCacheMode, 66 | serverSideCacheMode, 67 | isServerSide, 68 | }); 69 | 70 | const sessionAccessToken = currentAccessToken || session.accessToken; 71 | 72 | const authHeaders: any = {}; 73 | if (sessionAccessToken) { 74 | authHeaders.Authorization = `Bearer ${sessionAccessToken}`; 75 | } 76 | currentFetchOptions = { 77 | headers: { 78 | ...authHeaders, 79 | }, 80 | }; 81 | 82 | const existingClient = isServerSide 83 | ? false 84 | : graphQLClients.find((c) => c.id === id); 85 | if (existingClient) { 86 | existingClient.fetchOptions = fetchOptions; 87 | log("found existing client", { 88 | isServerSide, 89 | existingClient, 90 | }); 91 | return existingClient; 92 | } 93 | 94 | const subscriptionClient = createGQLClient({ 95 | url: graphql.ws, 96 | connectionParams: () => { 97 | return fetchOptions(); 98 | }, 99 | on: { 100 | connected: (socket: any) => { 101 | log("socket connected", socket); 102 | const gracefullyRestartSubscriptionsClient = () => { 103 | if (socket.readyState === WebSocket.OPEN) { 104 | log("restart subscription client"); 105 | socket.close(4205, "Client Restart"); 106 | } 107 | }; 108 | 109 | // just in case you were eager to restart 110 | if (restartRequired) { 111 | restartRequired = false; 112 | gracefullyRestartSubscriptionsClient(); 113 | } 114 | }, 115 | }, 116 | webSocketImpl: isServerSide ? ws : WebSocket, 117 | }); 118 | 119 | const ssr = ssrExchange({ 120 | isClient: !isServerSide, 121 | initialState: !isServerSide ? (window as any).__URQL_DATA__ : undefined, 122 | }); 123 | 124 | const serverExchanges = [dedupExchange, cacheExchange, ssr, fetchExchange]; 125 | 126 | const clientExchanges = [ 127 | dedupExchange, 128 | cacheExchange, 129 | ssr, 130 | fetchExchange, 131 | subscriptionExchange({ 132 | forwardSubscription(operation) { 133 | return { 134 | subscribe: (sink) => { 135 | const dispose = subscriptionClient.subscribe(operation, sink); 136 | return { 137 | unsubscribe: dispose, 138 | }; 139 | }, 140 | }; 141 | }, 142 | }), 143 | ]; 144 | 145 | const serverConfig = { 146 | url: graphql.httpInternal || graphql.http, 147 | preferGetMethod: false, 148 | fetchOptions, 149 | fetch, 150 | exchanges: serverExchanges, 151 | requestPolicy: serverSideCacheMode, 152 | }; 153 | 154 | const clientConfig = { 155 | url: graphql.http, 156 | preferGetMethod: false, 157 | fetchOptions, 158 | fetch, 159 | exchanges: clientExchanges, 160 | requestPolicy: clientSideCacheMode, 161 | }; 162 | 163 | const client = isServerSide 164 | ? createClient(serverConfig as any) 165 | : createClient(clientConfig as any); 166 | 167 | Object.assign(client, { id }); 168 | log("created client", { isServerSide, id, cacheMode: client.requestPolicy }); 169 | 170 | if (!isServerSide) { 171 | graphQLClients.push(client); 172 | } 173 | 174 | return client; 175 | }; 176 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as Web3Auth, 3 | // @ts-ignore 4 | isLoading, 5 | // @ts-ignore 6 | isAuthenticated, 7 | // @ts-ignore 8 | accessToken, 9 | // @ts-ignore 10 | idToken, 11 | // @ts-ignore 12 | refreshToken, 13 | // @ts-ignore 14 | userInfo, 15 | // @ts-ignore 16 | authError, 17 | } from "./web3auth/Web3Auth.svelte"; 18 | export { default as LoginButton } from "./web3auth/LoginButton.svelte"; 19 | export { default as LogoutButton } from "./web3auth/LogoutButton.svelte"; 20 | export { default as RefreshTokenButton } from "./web3auth/RefreshTokenButton.svelte"; 21 | export { default as ProtectedRoute } from "./web3auth/ProtectedRoute.svelte"; 22 | export { 23 | renewWeb3AuthToken, 24 | createAuthSession, 25 | endAuthSession, 26 | getUsers, 27 | registerUser, 28 | } from "./web3auth/auth-api"; 29 | export { userDetailsGenerator, getUserSession } from "./web3auth/hooks"; 30 | export { parseCookie } from "./web3auth/cookie"; 31 | export { getServerOnlyEnvVar } from "./getServerOnlyEnvVar"; 32 | export { post as getUsersPostHandler } from "./web3auth/routes/auth/users/index"; 33 | export { post as registerUserPostHandler } from "./web3auth/routes/auth/users/register"; 34 | export { post as loginPostHandler } from "./web3auth/routes/auth/login"; 35 | export { post as logoutPostHandler } from "./web3auth/routes/auth/logout"; 36 | export { post as refreshTokenPostHandler } from "./web3auth/routes/auth/refresh-token"; 37 | -------------------------------------------------------------------------------- /src/lib/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { RequestEvent } from "@sveltejs/kit/types/hooks"; 2 | 3 | export type AuthError = { 4 | error: string; 5 | errorDescription: string; 6 | }; 7 | export interface Locals { 8 | userid: string; 9 | accessToken: string; 10 | refreshToken: string; 11 | idToken: string; 12 | authError?: AuthError; 13 | user?: any; 14 | retries?: number; 15 | cookieAttributes?: string; 16 | } 17 | 18 | export type Web3AuthContextClientFn = ( 19 | request_path?: string, 20 | request_params?: Record 21 | ) => { 22 | session: any; 23 | issuer: string; 24 | page: Page; 25 | clientId: string; 26 | }; 27 | 28 | export type Web3AuthContextClientPromise = Promise; 29 | 30 | export interface Web3AuthSuccessResponse { 31 | accessToken: string; 32 | idToken: string; 33 | refreshToken: string; 34 | } 35 | 36 | export type Web3AuthFailureResponse = AuthError; 37 | 38 | export type Web3AuthResponse = Web3AuthSuccessResponse & 39 | Web3AuthFailureResponse; 40 | 41 | export interface UserDetailsGeneratorFn { 42 | (event: RequestEvent): AsyncGenerator>; 43 | } 44 | export interface UserSession { 45 | user: any; 46 | accessToken: string; 47 | refreshToken: string; 48 | userid: string; 49 | error?: AuthError | undefined; 50 | authServerOnline: boolean; 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/web3auth/LoginButton.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/lib/web3auth/LogoutButton.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 27 | -------------------------------------------------------------------------------- /src/lib/web3auth/ProtectedRoute.svelte: -------------------------------------------------------------------------------- 1 | 42 | 43 | {#await loadUser()} 44 |

Loading...

45 | {:then} 46 | {#if $isAuthenticated} 47 | 48 | {:else} 49 | 404 50 | {/if} 51 | {/await} 52 | -------------------------------------------------------------------------------- /src/lib/web3auth/RefreshTokenButton.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /src/lib/web3auth/Web3Auth.svelte: -------------------------------------------------------------------------------- 1 | 333 | 334 | 520 | 521 | 522 | -------------------------------------------------------------------------------- /src/lib/web3auth/auth-api.ts: -------------------------------------------------------------------------------- 1 | import type { Web3AuthResponse } from "../types"; 2 | import debug from "debug"; 3 | 4 | const log = debug("sveltekit-web3auth:lib/web3auth/auth-api"); 5 | 6 | // Auth Server API Calls 7 | 8 | export async function createAuthSession( 9 | issuer: string, 10 | clientId: string, 11 | clientSecret: string, 12 | address: string, 13 | signature: string 14 | ): Promise { 15 | let auth; 16 | const Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`; 17 | const createAuthSessionFetch = await fetch(`${issuer}/api/auth`, { 18 | body: JSON.stringify({ address, signature }), 19 | headers: { 20 | Authorization, 21 | "Content-Type": "application/json", 22 | }, 23 | method: "POST", 24 | }); 25 | 26 | if (createAuthSessionFetch.ok) { 27 | auth = await createAuthSessionFetch.json(); 28 | const data: Web3AuthResponse = { 29 | ...auth, 30 | }; 31 | return data; 32 | } 33 | } 34 | 35 | export async function endAuthSession(options: { 36 | issuer: string; 37 | clientId: string; 38 | clientSecret: string; 39 | address: string; 40 | }): Promise { 41 | const { issuer, clientId, clientSecret, address } = options; 42 | 43 | let auth; 44 | const Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`; 45 | 46 | const logoutRequest = await fetch(`${issuer}/api/auth/logout`, { 47 | body: JSON.stringify({ address }), 48 | headers: { 49 | Authorization, 50 | "Content-Type": "application/json", 51 | }, 52 | method: "POST", 53 | }); 54 | 55 | if (logoutRequest.ok) { 56 | auth = await logoutRequest.json(); 57 | const data = { 58 | ...auth, 59 | }; 60 | return data; 61 | } 62 | } 63 | 64 | export async function getUsers( 65 | issuer: string, 66 | clientId: string, 67 | clientSecret: string, 68 | address: string 69 | ): Promise { 70 | const Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`; 71 | 72 | const fetchResult = await fetch(`${issuer}/api/users?address=${address}`, { 73 | headers: { 74 | Authorization, 75 | }, 76 | method: "GET", 77 | }); 78 | 79 | if (fetchResult.ok) { 80 | const users = await fetchResult.json(); 81 | return users; 82 | } 83 | } 84 | 85 | export async function registerUser( 86 | issuer: string, 87 | clientId: string, 88 | clientSecret: string, 89 | address: string 90 | ): Promise { 91 | const Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`; 92 | 93 | const fetchResult = await fetch(`${issuer}/api/users`, { 94 | body: JSON.stringify({ address }), 95 | headers: { 96 | Authorization, 97 | "Content-Type": "application/json", 98 | }, 99 | method: "POST", 100 | }); 101 | 102 | if (fetchResult.ok) { 103 | const user = await fetchResult.json(); 104 | return user; 105 | } 106 | } 107 | 108 | export async function renewWeb3AuthToken( 109 | refreshToken: string, 110 | issuer: string, 111 | clientId: string, 112 | clientSecret: string 113 | ): Promise { 114 | log("renewing tokens"); 115 | if (!refreshToken) { 116 | const error_data: Web3AuthResponse = { 117 | error: "invalid_grant", 118 | errorDescription: "Invalid tokens", 119 | accessToken: null, 120 | refreshToken: null, 121 | idToken: null, 122 | }; 123 | return error_data; 124 | } 125 | 126 | const Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`; 127 | const res = await fetch(`${issuer}/api/auth/token`, { 128 | method: "POST", 129 | headers: { 130 | Authorization, 131 | "Content-Type": "application/json", 132 | }, 133 | body: JSON.stringify({ refreshToken }), 134 | }); 135 | 136 | if (res.ok) { 137 | const newTokens = await res.json(); 138 | const data: Web3AuthResponse = { 139 | ...newTokens, 140 | }; 141 | return data; 142 | } else { 143 | const data: Web3AuthResponse = await res.json(); 144 | console.error("renew response not ok", data); 145 | return data; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/lib/web3auth/cookie.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * RegExp to match field-content in RFC 7230 sec 3.2 3 | * 4 | * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] 5 | * field-vchar = VCHAR / obs-text 6 | * obs-text = %x80-FF 7 | */ 8 | 9 | // const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; 10 | 11 | // const encode = encodeURIComponent; 12 | export const parseCookie = (str: string, options?: any) => { 13 | const decode = decodeURIComponent; 14 | const pairSplitRegExp = /; */; 15 | 16 | if (typeof str !== "string") { 17 | throw new TypeError("argument str must be a string"); 18 | } 19 | 20 | const obj = {}; 21 | const opt = options || {}; 22 | const pairs = str.split(pairSplitRegExp); 23 | const dec = opt.decode || decode; 24 | 25 | for (let i = 0; i < pairs.length; i++) { 26 | const pair = pairs[i]; 27 | let eq_idx = pair.indexOf("="); 28 | 29 | // skip things that don't look like key=value 30 | if (eq_idx < 0) { 31 | continue; 32 | } 33 | 34 | const key = pair.substr(0, eq_idx).trim(); 35 | let val = pair.substr(++eq_idx, pair.length).trim(); 36 | 37 | // quoted values 38 | if ('"' == val[0]) { 39 | val = val.slice(1, -1); 40 | } 41 | 42 | // only assign once 43 | if (undefined == obj[key]) { 44 | obj[key] = tryDecode(val, dec); 45 | } 46 | } 47 | 48 | return obj; 49 | }; 50 | 51 | /** 52 | * Try decoding a string using a decoding function. 53 | * 54 | * @param {string} str 55 | * @param {function} decode 56 | * @private 57 | */ 58 | 59 | function tryDecode(str, decode) { 60 | try { 61 | return decode(str); 62 | } catch (e) { 63 | return str; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/lib/web3auth/hooks.ts: -------------------------------------------------------------------------------- 1 | import type { Locals, UserDetailsGeneratorFn } from "../types"; 2 | import { parseCookie } from "./cookie"; 3 | import { isTokenExpired } from "./jwt"; 4 | import { renewWeb3AuthToken } from "./auth-api"; 5 | import { 6 | injectCookies, 7 | isAuthInfoInvalid, 8 | parseUser, 9 | populateResponseHeaders, 10 | populateRequestLocals, 11 | setRequestLocalsFromNewTokens, 12 | } from "./server-utils"; 13 | import debug from "debug"; 14 | import type { RequestEvent } from "@sveltejs/kit"; 15 | 16 | const log = debug("sveltekit-web3auth:lib/web3auth/hooks"); 17 | 18 | // This function is recursive - if a user does not have an access token, but a refresh token 19 | // it attempts to refresh the access token, and calls itself again recursively, this time 20 | // to go down the path of having the access token 21 | export const getUserSession = async ( 22 | event: RequestEvent, 23 | issuer, 24 | clientId, 25 | clientSecret, 26 | refreshTokenMaxRetries 27 | ) => { 28 | const { request } = event; 29 | try { 30 | if ( 31 | event.locals?.accessToken && 32 | !isTokenExpired(event.locals?.accessToken) && 33 | event.locals?.user && 34 | event.locals?.userid 35 | ) { 36 | log("has valid access token and user information set - returning"); 37 | const userClone = Object.assign({}, event.locals.user); 38 | if (userClone?.username) { 39 | userClone.username = decodeURI(userClone.username); 40 | } 41 | return { 42 | user: userClone, 43 | accessToken: event.locals.accessToken, 44 | refreshToken: event.locals.refreshToken, 45 | userid: event.locals.user.address, 46 | authServerOnline: true, 47 | }; 48 | } else { 49 | log("get user session - no access token present"); 50 | 51 | // Check auth server is ready 52 | try { 53 | const testAuthServerResponse = await fetch(issuer, { 54 | headers: { 55 | "Content-Type": "application/json", 56 | }, 57 | }); 58 | if (!testAuthServerResponse.ok) { 59 | throw { 60 | error: await testAuthServerResponse.json(), 61 | }; 62 | } 63 | } catch (e) { 64 | throw { 65 | error: "auth_server_conn_error", 66 | errorDescription: "Auth Server Connection Error", 67 | }; 68 | } 69 | 70 | // try to refresh 71 | try { 72 | if ( 73 | event.locals?.refreshToken && 74 | event.locals?.retries < refreshTokenMaxRetries 75 | ) { 76 | log("attempting to exchange refresh token", event.locals?.retries); 77 | const tokenSet = await renewWeb3AuthToken( 78 | event.locals.refreshToken, 79 | issuer, 80 | clientId, 81 | clientSecret 82 | ); 83 | 84 | if (tokenSet?.error) { 85 | throw { 86 | error: tokenSet.error, 87 | errorDescription: tokenSet.errorDescription, 88 | }; 89 | } 90 | 91 | setRequestLocalsFromNewTokens(event, tokenSet); 92 | 93 | event.locals.retries = event.locals.retries + 1; 94 | return await getUserSession( 95 | event, 96 | issuer, 97 | clientId, 98 | clientSecret, 99 | refreshTokenMaxRetries 100 | ); 101 | } 102 | } catch (e) { 103 | throw { 104 | error: e.error || "token_refresh_error", 105 | errorDescription: `Unable to exchange refresh token: ${e.errorDescription}`, 106 | }; 107 | } 108 | 109 | log("no refresh token, or max retries reached"); 110 | // no access token or refresh token 111 | throw { 112 | error: "missing_jwt", 113 | errorDescription: "access token not found or is null", 114 | }; 115 | } 116 | } catch (err) { 117 | log("returning without user info"); 118 | event.locals.accessToken = ""; 119 | event.locals.refreshToken = ""; 120 | event.locals.userid = ""; 121 | event.locals.user = null; 122 | if (err?.error) { 123 | event.locals.authError.error = err.error; 124 | } 125 | if (err?.errorDescription) { 126 | event.locals.authError.errorDescription = err.errorDescription; 127 | } 128 | return { 129 | user: null, 130 | accessToken: null, 131 | refreshToken: null, 132 | userid: null, 133 | error: event.locals.authError?.error ? event.locals.authError : null, 134 | authServerOnline: err.error !== "auth_server_conn_error" ? true : false, 135 | }; 136 | } 137 | }; 138 | 139 | export const userDetailsGenerator: UserDetailsGeneratorFn = async function* ( 140 | event: RequestEvent 141 | ) { 142 | const { request } = event; 143 | const cookies = request.headers.get("cookie") 144 | ? parseCookie(request.headers.get("cookie") || "") 145 | : null; 146 | 147 | const userInfo = cookies?.["userInfo"] 148 | ? JSON.parse(cookies?.["userInfo"]) 149 | : {}; 150 | 151 | event.locals.retries = 0; 152 | event.locals.authError = { 153 | error: null, 154 | errorDescription: null, 155 | }; 156 | 157 | populateRequestLocals(event, "userid", userInfo, ""); 158 | populateRequestLocals(event, "accessToken", userInfo, null); 159 | populateRequestLocals(event, "refreshToken", userInfo, null); 160 | 161 | // Parsing user object 162 | const userJsonParseFailed = parseUser(event, userInfo); 163 | const tokenExpired = isTokenExpired(event.locals.accessToken); 164 | const beforeAccessToken = event.locals.accessToken; 165 | 166 | event = { ...event, ...(yield) }; 167 | 168 | const response = { status: 200, headers: {} }; 169 | const afterAccessToken = event.locals.accessToken; 170 | 171 | if (isAuthInfoInvalid(request.headers) || tokenExpired) { 172 | populateResponseHeaders(event, response); 173 | } 174 | 175 | if ( 176 | isAuthInfoInvalid(userInfo) || 177 | (event.locals?.user && userJsonParseFailed) || 178 | tokenExpired || 179 | beforeAccessToken !== afterAccessToken 180 | ) { 181 | // set a cookie so that we recognize future requests 182 | injectCookies(event, response); 183 | } 184 | 185 | log("returning response with injected cookies"); 186 | return response; 187 | }; 188 | -------------------------------------------------------------------------------- /src/lib/web3auth/jwt.ts: -------------------------------------------------------------------------------- 1 | export const getTokenData = (jwt: string): any => { 2 | let data; 3 | try { 4 | data = JSON.parse(Buffer.from(jwt.split(".")[1], "base64").toString()); 5 | } catch (e) { 6 | try { 7 | data = JSON.parse(atob(jwt.split(".")[1]).toString()); 8 | } catch (err) { 9 | return {}; 10 | } 11 | } 12 | 13 | return data; 14 | }; 15 | 16 | export function isTokenExpired(jwt: string): boolean { 17 | if (!jwt || jwt.length < 10) { 18 | return true; 19 | } 20 | const tokenTimeSkew = 10; // 10 seconds before actual token exp 21 | 22 | const data = getTokenData(jwt); 23 | const now = new Date().getTime() / 1000; 24 | const expirationTime = data?.exp || 0 - tokenTimeSkew; 25 | const isExpired = now > expirationTime; 26 | return isExpired; 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/web3auth/metamask.ts: -------------------------------------------------------------------------------- 1 | export const handleSignMessage = 2 | (web3) => 3 | async ({ address, nonce }: { address: string; nonce: string }) => { 4 | try { 5 | const signature = await web3?.eth.personal.sign( 6 | `I am signing my one-time nonce: ${nonce}`, 7 | address, 8 | "" // MetaMask will ignore the password argument here 9 | ); 10 | 11 | return { address, signature }; 12 | } catch (err) { 13 | throw new Error( 14 | `You need to sign the message to be able to log in. ${err}` 15 | ); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/web3auth/routes-api.ts: -------------------------------------------------------------------------------- 1 | // SvelteKit server API calls 2 | 3 | export const handleAuthenticate = 4 | (clientId) => 5 | ({ address, signature }: { address: string; signature: string }) => 6 | fetch(`/auth/login`, { 7 | body: JSON.stringify({ clientId, address, signature }), 8 | headers: { 9 | "Content-Type": "application/json", 10 | }, 11 | method: "POST", 12 | }).then((response) => response.json()); 13 | 14 | export const handleSignup = (clientId: string) => (address: string) => 15 | fetch(`/auth/users/register`, { 16 | body: JSON.stringify({ address, clientId }), 17 | headers: { 18 | "Content-Type": "application/json", 19 | }, 20 | method: "POST", 21 | }).then((response) => response.json()); 22 | -------------------------------------------------------------------------------- /src/lib/web3auth/routes/auth/login.ts: -------------------------------------------------------------------------------- 1 | import type { RequestEvent } from "@sveltejs/kit"; 2 | import jwtDecode from "jwt-decode"; 3 | import { createAuthSession } from "$lib/web3auth/auth-api"; 4 | import debug from "debug"; 5 | 6 | const log = debug("sveltekit-web3auth:/auth/login"); 7 | 8 | export const post = (clientSecret, issuer) => async (event: RequestEvent) => { 9 | log("logging in"); 10 | const { request } = event; 11 | const body: any = await request.json(); 12 | const clientId = body.clientId; 13 | const address = body.address; 14 | const signature = body.signature; 15 | 16 | log("logging in", { clientId, address }); 17 | 18 | const auth = await createAuthSession( 19 | issuer, 20 | clientId, 21 | clientSecret, 22 | address, 23 | signature 24 | ); 25 | 26 | log("logged in", { clientId, address }); 27 | 28 | const user: any = jwtDecode(auth.idToken); 29 | delete user.aud; 30 | delete user.exp; 31 | delete user.iat; 32 | delete user.iss; 33 | delete user.sub; 34 | delete user.typ; 35 | 36 | const response = { 37 | body: { 38 | ...auth, 39 | }, 40 | }; 41 | 42 | // Cookie is set based on locals value in next step 43 | event.locals.userid = user.address; 44 | event.locals.user = user; 45 | event.locals.accessToken = auth.accessToken; 46 | event.locals.refreshToken = auth.refreshToken; 47 | 48 | return response; 49 | }; 50 | -------------------------------------------------------------------------------- /src/lib/web3auth/routes/auth/logout.ts: -------------------------------------------------------------------------------- 1 | import type { RequestEvent } from "@sveltejs/kit"; 2 | import { endAuthSession } from "../../auth-api"; 3 | import { parseCookie } from "../../cookie"; 4 | import debug from "debug"; 5 | 6 | const log = debug("sveltekit-web3auth:/auth/logout"); 7 | 8 | export const post = (clientSecret, issuer) => async (event: RequestEvent) => { 9 | log("logging out"); 10 | const { request } = event; 11 | const body: any = await request.json(); 12 | const clientId = body.clientId; 13 | const cookie: any = parseCookie(request.headers.get("cookie")); 14 | const { userInfo } = cookie; 15 | const user = JSON.parse(userInfo); 16 | const address = user.userid; 17 | 18 | log("logging out", { clientId, address }); 19 | 20 | const auth = await endAuthSession({ 21 | issuer, 22 | clientId, 23 | clientSecret, 24 | address, 25 | }); 26 | 27 | log("logged out", { clientId, address }); 28 | 29 | const response = { 30 | body: {}, 31 | }; 32 | 33 | // Cookie is set based on locals value in next step 34 | event.locals.userid = null; 35 | event.locals.user = null; 36 | event.locals.accessToken = null; 37 | event.locals.refreshToken = null; 38 | 39 | return response; 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/web3auth/routes/auth/refresh-token.ts: -------------------------------------------------------------------------------- 1 | import type { RequestEvent } from "@sveltejs/kit"; 2 | import { renewWeb3AuthToken } from "../../auth-api"; 3 | import { parseCookie } from "../../cookie"; 4 | import { setRequestLocalsFromNewTokens } from "../../server-utils"; 5 | import debug from "debug"; 6 | 7 | const log = debug("sveltekit-web3auth:/auth/refresh-token"); 8 | 9 | export const post = (clientSecret, issuer) => async (event: RequestEvent) => { 10 | const { request } = event; 11 | const body: any = await request.json(); 12 | const clientId = body.clientId; 13 | const cookie: any = parseCookie(request.headers.get("cookie")); 14 | const { userInfo } = cookie; 15 | const user = JSON.parse(userInfo); 16 | 17 | const auth = await renewWeb3AuthToken( 18 | user.refreshToken, 19 | issuer, 20 | clientId, 21 | clientSecret 22 | ); 23 | 24 | setRequestLocalsFromNewTokens(event, auth); 25 | 26 | const response = { 27 | body: { 28 | ...auth, 29 | }, 30 | }; 31 | 32 | return response; 33 | }; 34 | -------------------------------------------------------------------------------- /src/lib/web3auth/routes/auth/users/index.ts: -------------------------------------------------------------------------------- 1 | import type { RequestEvent } from "@sveltejs/kit"; 2 | import { getUsers } from "../../../auth-api"; 3 | import debug from "debug"; 4 | 5 | const log = debug("sveltekit-web3auth:/auth/users"); 6 | 7 | export const post = 8 | (clientSecret, issuer) => 9 | async ({ params, request }: RequestEvent) => { 10 | log("getting users for address"); 11 | 12 | const body: any = await request.json(); 13 | const clientId = body.clientId; 14 | const address = body.address; 15 | 16 | log("getting users for address", { clientId, address }); 17 | 18 | const users = await getUsers(issuer, clientId, clientSecret, address); 19 | 20 | log("user query result", { clientId, address, users }); 21 | 22 | const response = { 23 | body: JSON.stringify(users), 24 | }; 25 | 26 | return response; 27 | }; 28 | -------------------------------------------------------------------------------- /src/lib/web3auth/routes/auth/users/register.ts: -------------------------------------------------------------------------------- 1 | import type { RequestEvent } from "@sveltejs/kit"; 2 | import { registerUser } from "../../../auth-api"; 3 | 4 | import debug from "debug"; 5 | 6 | const log = debug("sveltekit-web3auth:/auth/users/register"); 7 | 8 | export const post = 9 | (clientSecret, issuer) => 10 | async ({ params, request }: RequestEvent) => { 11 | log("registering user with address"); 12 | 13 | const body: any = await request.json(); 14 | const clientId = body.clientId; 15 | const address = body.address; 16 | 17 | log("registering user with address", { clientId, address }); 18 | 19 | const user = await registerUser(issuer, clientId, clientSecret, address); 20 | 21 | log("registered", { clientId, address }); 22 | 23 | const response = { 24 | body: JSON.stringify(user), 25 | }; 26 | 27 | return response; 28 | }; 29 | -------------------------------------------------------------------------------- /src/lib/web3auth/server-utils.ts: -------------------------------------------------------------------------------- 1 | import jwtDecode from "jwt-decode"; 2 | import type { Locals } from "../types"; 3 | import type { RequestEvent } from "@sveltejs/kit/types/hooks"; 4 | import debug from "debug"; 5 | 6 | const log = debug("sveltekit-web3auth:server-utils"); 7 | 8 | export const injectCookies = (event: RequestEvent, response) => { 9 | let responseCookies = {}; 10 | let serialized_user = null; 11 | 12 | try { 13 | if (event?.locals?.user?.username) { 14 | event.locals.user.username = encodeURI(event?.locals?.user?.username); 15 | } 16 | serialized_user = JSON.stringify(event.locals.user); 17 | } catch { 18 | event.locals.user = null; 19 | } 20 | 21 | responseCookies = { 22 | userid: `${event.locals.userid}`, 23 | user: `${serialized_user}`, 24 | }; 25 | responseCookies["refreshToken"] = `${event.locals.refreshToken}`; 26 | let cookieAtrributes = "Path=/; HttpOnly;"; 27 | if (event.locals?.cookieAttributes) { 28 | cookieAtrributes = event.locals.cookieAttributes; 29 | } 30 | 31 | response.headers["set-cookie"] = `userInfo=${JSON.stringify( 32 | responseCookies 33 | )}; ${cookieAtrributes}`; 34 | }; 35 | 36 | export const isAuthInfoInvalid = (obj) => { 37 | const isAuthInvalid = 38 | !obj?.userid || !obj?.accessToken || !obj?.refreshToken || !obj?.user; 39 | return isAuthInvalid; 40 | }; 41 | 42 | export const parseUser = (event: RequestEvent, userInfo) => { 43 | const { request } = event; 44 | let userJsonParseFailed = false; 45 | try { 46 | if (request.headers?.get("user")) { 47 | event.locals.user = JSON.parse(request.headers.get("user")); 48 | } else { 49 | if ( 50 | userInfo?.user && 51 | userInfo?.user !== "null" && 52 | userInfo?.user !== "undefined" 53 | ) { 54 | event.locals.user = JSON.parse(userInfo.user); 55 | if (!event.locals.user) { 56 | userJsonParseFailed = true; 57 | } 58 | } else { 59 | throw { 60 | error: "invalid_user_object", 61 | }; 62 | } 63 | } 64 | } catch { 65 | userJsonParseFailed = true; 66 | event.locals.user = null; 67 | } 68 | return userJsonParseFailed; 69 | }; 70 | 71 | export const populateRequestLocals = ( 72 | event: RequestEvent, 73 | keyName: string, 74 | userInfo, 75 | defaultValue 76 | ) => { 77 | const { request } = event; 78 | if (request.headers.get(keyName)) { 79 | event.locals[keyName] = request.headers.get(keyName); 80 | } else { 81 | if ( 82 | userInfo[keyName] && 83 | userInfo[keyName] !== "null" && 84 | userInfo[keyName] !== "undefined" 85 | ) { 86 | event.locals[keyName] = userInfo[keyName]; 87 | } else { 88 | event.locals[keyName] = defaultValue; 89 | } 90 | } 91 | return request; 92 | }; 93 | 94 | export const populateResponseHeaders = ( 95 | event: RequestEvent, 96 | response 97 | ) => { 98 | if (event.locals.user) { 99 | response.headers["user"] = `${JSON.stringify(event.locals.user)}`; 100 | } 101 | 102 | if (event.locals.userid) { 103 | response.headers["userid"] = `${event.locals.userid}`; 104 | } 105 | 106 | if (event.locals.accessToken) { 107 | response.headers["accessToken"] = `${event.locals.accessToken}`; 108 | } 109 | if (event.locals.refreshToken) { 110 | response.headers["refreshToken"] = `${event.locals.refreshToken}`; 111 | } 112 | }; 113 | 114 | export const setRequestLocalsFromNewTokens = ( 115 | event: RequestEvent, 116 | tokenSet: { accessToken: string; idToken: string; refreshToken: string } 117 | ) => { 118 | const parsedUserInfo: any = jwtDecode(tokenSet.idToken); 119 | delete parsedUserInfo.aud; 120 | delete parsedUserInfo.exp; 121 | delete parsedUserInfo.iat; 122 | delete parsedUserInfo.iss; 123 | delete parsedUserInfo.sub; 124 | delete parsedUserInfo.typ; 125 | 126 | // Cookie is set based on locals value in next step 127 | event.locals.userid = parsedUserInfo.address; 128 | event.locals.user = parsedUserInfo; 129 | event.locals.accessToken = tokenSet.accessToken; 130 | event.locals.refreshToken = tokenSet.refreshToken; 131 | }; 132 | -------------------------------------------------------------------------------- /src/routes/__layout.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/routes/auth/login.ts: -------------------------------------------------------------------------------- 1 | import { getServerOnlyEnvVar } from "$lib"; 2 | import { post as login } from "$lib/web3auth/routes/auth/login"; 3 | import { config } from "../../config"; 4 | 5 | const clientSecret = 6 | getServerOnlyEnvVar(process, "WEB3AUTH_CLIENT_SECRET") || 7 | config.web3auth.clientSecret; 8 | const issuer = config.web3auth.issuer; 9 | 10 | export const post = login(clientSecret, issuer); 11 | -------------------------------------------------------------------------------- /src/routes/auth/logout.ts: -------------------------------------------------------------------------------- 1 | import { getServerOnlyEnvVar } from "$lib"; 2 | import { post as logout } from "$lib/web3auth/routes/auth/logout"; 3 | import { config } from "../../config"; 4 | 5 | const clientSecret = 6 | getServerOnlyEnvVar(process, "WEB3AUTH_CLIENT_SECRET") || 7 | config.web3auth.clientSecret; 8 | const issuer = config.web3auth.issuer; 9 | 10 | export const post = logout(clientSecret, issuer); 11 | -------------------------------------------------------------------------------- /src/routes/auth/refresh-token.ts: -------------------------------------------------------------------------------- 1 | import { getServerOnlyEnvVar } from "$lib"; 2 | import { post as refreshToken } from "$lib/web3auth/routes/auth/refresh-token"; 3 | import { config } from "../../config"; 4 | 5 | const clientSecret = 6 | getServerOnlyEnvVar(process, "WEB3AUTH_CLIENT_SECRET") || 7 | config.web3auth.clientSecret; 8 | const issuer = config.web3auth.issuer; 9 | 10 | export const post = refreshToken(clientSecret, issuer); 11 | -------------------------------------------------------------------------------- /src/routes/auth/users/index.ts: -------------------------------------------------------------------------------- 1 | import { getServerOnlyEnvVar } from "$lib"; 2 | import { post as getUsers } from "$lib/web3auth/routes/auth/users/index"; 3 | import { config } from "../../../config"; 4 | 5 | const clientSecret = 6 | getServerOnlyEnvVar(process, "WEB3AUTH_CLIENT_SECRET") || 7 | config.web3auth.clientSecret; 8 | const issuer = config.web3auth.issuer; 9 | 10 | export const post = getUsers(clientSecret, issuer); 11 | -------------------------------------------------------------------------------- /src/routes/auth/users/register.ts: -------------------------------------------------------------------------------- 1 | import { getServerOnlyEnvVar } from "$lib"; 2 | import { post as register } from "$lib/web3auth/routes/auth/users/register"; 3 | import { config } from "../../../config"; 4 | 5 | const clientSecret = 6 | getServerOnlyEnvVar(process, "WEB3AUTH_CLIENT_SECRET") || 7 | config.web3auth.clientSecret; 8 | const issuer = config.web3auth.issuer; 9 | 10 | export const post = register(clientSecret, issuer); 11 | -------------------------------------------------------------------------------- /src/routes/index.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 |
42 |

45 | Sveltekit + web3auth 46 |

47 | 48 | {#if $isAuthenticated} 49 |
52 |
Access Token
53 |
54 |

59 | {$accessToken} 60 |

61 |
62 | 66 | {#if isAccessTokenCopied} 67 |
68 | Copied! 69 |
70 | {/if} 71 |
72 |
73 | Logout 74 | Refresh Tokens 77 |
78 | {:else if $authError} 79 |
80 | {$authError?.errorDescription} 81 |
82 | {:else if $isLoading} 83 |
86 | Loading ... 87 |
88 | {:else} 89 |
92 |

NO AUTH AVAILABLE

93 | Login 94 |
95 | {/if} 96 |
97 | -------------------------------------------------------------------------------- /src/routes/profile/index.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 |
10 |

Your Profile

11 |

Address: {$session.user?.address}

12 |

Username: {$session.user?.username}

13 | {#if $session.user.roles} 14 |
15 | Your roles: 16 |
    17 | {#each $session?.user?.roles as role} 18 |
  • {role}
  • 19 | {/each} 20 |
21 |
22 | {/if} 23 | Logout 24 |
25 |
26 | -------------------------------------------------------------------------------- /src/routes/todos/index.svelte: -------------------------------------------------------------------------------- 1 | 163 | 164 | 330 | 331 | 332 |
335 |
336 |
337 |

{$session.user.address}'s Todo List

338 |
342 | 348 | 352 |
353 |
354 |
355 | {#if !todos || (todos && todos.length === 0)} 356 |

No todos

357 | {:else} 358 |
    359 | {#each todos as todo} 360 | 366 | {/each} 367 |
368 |

{count} Total

369 | {/if} 370 |
371 |
372 |
373 |
374 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeEntrepreneur/sveltekit-web3auth/d47ca2f3d16aa7015382e71d24e8e641d5b2e395/static/favicon.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import node from "@sveltejs/adapter-node"; 2 | import preprocess from "svelte-preprocess"; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://github.com/sveltejs/svelte-preprocess 7 | // for more information about preprocessors 8 | preprocess: [ 9 | preprocess({ 10 | postcss: true, 11 | }), 12 | ], 13 | kit: { 14 | adapter: node(), 15 | package: { 16 | dir: "sveltekit-web3auth", 17 | }, 18 | vite: { 19 | ssr: { 20 | noExternal: Object.keys({}), 21 | }, 22 | optimizeDeps: { 23 | exclude: ["@urql/svelte", "node-fetch"], 24 | }, 25 | }, 26 | }, 27 | }; 28 | 29 | export default config; 30 | // Workaround until SvelteKit uses Vite 2.3.8 (and it's confirmed to fix the Tailwind JIT problem) 31 | const mode = process.env.NODE_ENV; 32 | const dev = mode === "development"; 33 | process.env.TAILWIND_MODE = dev ? "watch" : "build"; 34 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | content: ["./src/**/*.{html,js,svelte,ts}"], 3 | mode: "jit", 4 | 5 | theme: { 6 | extend: {}, 7 | }, 8 | 9 | plugins: [], 10 | }; 11 | 12 | module.exports = config; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "es2020", 5 | "lib": ["es2020"], 6 | "target": "es2019", 7 | /** 8 | svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript 9 | to enforce using \`import type\` instead of \`import\` for Types. 10 | */ 11 | "importsNotUsedAsValues": "error", 12 | "isolatedModules": true, 13 | "resolveJsonModule": true, 14 | /** 15 | To have warnings/errors of the Svelte compiler at the correct position, 16 | enable source maps by default. 17 | */ 18 | "sourceMap": true, 19 | "esModuleInterop": true, 20 | "skipLibCheck": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "baseUrl": ".", 23 | "allowJs": true, 24 | "checkJs": true, 25 | "paths": { 26 | "$lib": ["src/lib"], 27 | "$lib/*": ["src/lib/*"], 28 | "$components": ["src/components"], 29 | "$components/*": ["src/components/*"] 30 | } 31 | }, 32 | "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"] 33 | } 34 | --------------------------------------------------------------------------------