├── .env.example ├── .gitattributes ├── .gitignore ├── README.md ├── app.config.ts ├── capacitor.config.ts ├── components ├── access-denied.tsx ├── footer.module.css ├── footer.tsx ├── header.module.css ├── header.tsx ├── layout.tsx ├── login-button.module.css ├── login-button.tsx └── network-status.tsx ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── admin.tsx ├── api-example.tsx ├── api │ ├── auth │ │ └── [...nextauth].ts │ └── examples │ │ ├── jwt.ts │ │ ├── protected.ts │ │ └── session.ts ├── client.tsx ├── index.tsx ├── me.tsx ├── policy.tsx ├── protected.tsx └── styles.css ├── process.d.ts ├── tsconfig.json └── utils ├── helper.ts └── session.tsx /.env.example: -------------------------------------------------------------------------------- 1 | NEXTAUTH_URL= 2 | NEXTAUTH_SECRET= 3 | 4 | GITHUB_ID= 5 | GITHUB_SECRET= 6 | 7 | DATABASE_URL= 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules/ 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | .yarn-integrity 11 | .npm 12 | 13 | .eslintcache 14 | 15 | *.tsbuildinfo 16 | next-env.d.ts 17 | 18 | .next 19 | .vercel 20 | .env 21 | .env*.local 22 | 23 | out 24 | android -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to make Next Auth work with Capacitor 2 | ## Before you start 3 | **This is an example of a production use.** **It doesn't work straight out of the box on `localhost`**, as the application that runs on on Vercel doesn't allow requests from `localhost`. (See https://github.com/choutkamartin/next-auth-capacitor/issues/4) 4 | 5 | This is an example of how would you authenticate user from your built Android application against a Next.js application running on a distant server. 6 | 7 | ## What is this? 8 | If you have a Next.js application, you may know you can run it natively on Android or iOS using Capacitor. 9 | 10 | Next Auth however doesn't work straight out the box. This repository shows that you can use some workarounds to make it work. 11 | 12 | Basic Capacitor knowledge required. 13 | 14 | ## Tips 15 | 1. Use `chrome://inspect/#devices` to inspect the webview of your application. You can inspect cookies of your app this way. 16 | 2. Don't know much about Capacitor? Read this https://devdactic.com/nextjs-and-capacitor 17 | 18 | ## Caveats 19 | ### iOS and the forbidden usage of https scheme 20 | iOS as a system doesn't allow the usage of `https` scheme, which is something we rely on in this repository, as we want our application to have a hostname with the `https` scheme 21 | 22 | Read more at: https://forum.ionicframework.com/t/https-for-the-app-server-protocol-instead-of-capacitor-on-ios/200116/2 23 | 24 | 25 | ## What you need to do: 26 | ### 1. Change capacitor.config.ts to: 27 | ```ts 28 | server: { 29 | hostname: `mob.next-auth-capacitor.vercel.app`, // We need to change hostname to subdomain of our domain the API is hosted on 30 | androidScheme: "https", // HTTPS should be set preferably 31 | }, 32 | ``` 33 | 34 | We need to set hostname and androidScheme to `https` so we can share our cookies with our application (domain - subdomain cookies sharing). 35 | 36 | ### 2. Change Next API config (headers) to enable CORS 37 | You must change mainly these keys: `Access-Control-Allow-Credentials` and `Access-Control-Allow-Origin` so the application can connect to the server. 38 | ```js 39 | async headers() { 40 | return [ 41 | { 42 | source: "/:path*", 43 | headers: [ 44 | { key: "Access-Control-Allow-Credentials", value: "true" }, 45 | { 46 | key: "Access-Control-Allow-Origin", 47 | value: "https://mob.next-auth-capacitor.vercel.app", 48 | }, 49 | { 50 | key: "Access-Control-Allow-Methods", 51 | value: "GET,OPTIONS,PATCH,DELETE,POST,PUT", 52 | }, 53 | { 54 | key: "Access-Control-Allow-Headers", 55 | value: 56 | "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version", 57 | }, 58 | ], 59 | }, 60 | ]; 61 | }, 62 | ``` 63 | ### 3. Create own OAuth flow 64 | See `utils/helper.ts` 65 | 66 | ### 4. Create own UseSession provider 67 | See `utils/session.tsx` 68 | 69 | ### 5. Change `_app.tsx` SessionProvider to provider you just created 70 | See `pages/_app.tsx` 71 | 72 | ### 6. Modify Next Auth config 73 | See `pages/api/auth/[...nextauth].ts` 74 | 75 | --- 76 | Based on a discussion: https://github.com/nextauthjs/next-auth/discussions/4446 77 | Thanks to [@creativiii](https://github.com/creativiii) 78 | -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | const appConfig = { 2 | apiHost: "https://next-auth-capacitor.vercel.app", // Set the domain where your API is hosted on 3 | }; 4 | 5 | export default appConfig; 6 | -------------------------------------------------------------------------------- /capacitor.config.ts: -------------------------------------------------------------------------------- 1 | import { CapacitorConfig } from "@capacitor/cli"; 2 | import appConfig from "./app.config"; 3 | 4 | const config: CapacitorConfig = { 5 | appId: "com.nextauthcapacitor.app", 6 | appName: "Next Auth Capacitor", 7 | webDir: "out", 8 | bundledWebRuntime: false, 9 | server: { 10 | hostname: `mob.next-auth-capacitor.vercel.app`, // We need to change hostname to subdomain of our domain the API is hosted on 11 | androidScheme: "https", // HTTPS should be set preferably 12 | }, 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /components/access-denied.tsx: -------------------------------------------------------------------------------- 1 | import { signIn } from "next-auth/react"; 2 | 3 | /** 4 | * I did not make any changes to this code, so it may not work 5 | */ 6 | export default function AccessDenied() { 7 | return ( 8 | <> 9 |

Access Denied

10 |

11 | { 14 | e.preventDefault(); 15 | signIn(); 16 | }} 17 | > 18 | You must be signed in to view this page 19 | 20 |

21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /components/footer.module.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | margin-top: 2rem; 3 | } 4 | 5 | .navItems { 6 | margin-bottom: 1rem; 7 | padding: 0; 8 | list-style: none; 9 | } 10 | 11 | .navItem { 12 | display: inline-block; 13 | margin-right: 1rem; 14 | } 15 | -------------------------------------------------------------------------------- /components/footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | import styles from "./footer.module.css" 3 | import packageJSON from "../package.json" 4 | 5 | export default function Footer() { 6 | return ( 7 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /components/header.module.css: -------------------------------------------------------------------------------- 1 | /* Set min-height to avoid page reflow while session loading */ 2 | .signedInStatus { 3 | display: block; 4 | min-height: 4rem; 5 | width: 100%; 6 | } 7 | 8 | .loading, 9 | .loaded { 10 | position: relative; 11 | top: 0; 12 | opacity: 1; 13 | overflow: hidden; 14 | border-radius: 0 0 0.6rem 0.6rem; 15 | padding: 0.6rem 1rem; 16 | margin: 0; 17 | background-color: rgba(0, 0, 0, 0.05); 18 | transition: all 0.2s ease-in; 19 | } 20 | 21 | .loading { 22 | top: -2rem; 23 | opacity: 0; 24 | } 25 | 26 | .signedInText, 27 | .notSignedInText { 28 | position: absolute; 29 | padding-top: 0.8rem; 30 | left: 1rem; 31 | right: 6.5rem; 32 | white-space: nowrap; 33 | text-overflow: ellipsis; 34 | overflow: hidden; 35 | display: inherit; 36 | z-index: 1; 37 | line-height: 1.3rem; 38 | } 39 | 40 | .signedInText { 41 | padding-top: 0rem; 42 | left: 4.6rem; 43 | } 44 | 45 | .avatar { 46 | border-radius: 2rem; 47 | float: left; 48 | height: 2.8rem; 49 | width: 2.8rem; 50 | background-color: white; 51 | background-size: cover; 52 | background-repeat: no-repeat; 53 | } 54 | 55 | .button, 56 | .buttonPrimary { 57 | float: right; 58 | margin-right: -0.4rem; 59 | font-weight: 500; 60 | border-radius: 0.3rem; 61 | cursor: pointer; 62 | font-size: 1rem; 63 | line-height: 1.4rem; 64 | padding: 0.7rem 0.8rem; 65 | position: relative; 66 | z-index: 10; 67 | background-color: transparent; 68 | color: #555; 69 | } 70 | 71 | .buttonPrimary { 72 | background-color: #346df1; 73 | border-color: #346df1; 74 | color: #fff; 75 | text-decoration: none; 76 | padding: 0.7rem 1.4rem; 77 | } 78 | 79 | .buttonPrimary:hover { 80 | box-shadow: inset 0 0 5rem rgba(0, 0, 0, 0.2); 81 | } 82 | 83 | .navItems { 84 | margin-bottom: 2rem; 85 | padding: 0; 86 | list-style: none; 87 | } 88 | 89 | .navItem { 90 | display: inline-block; 91 | margin-right: 1rem; 92 | } 93 | -------------------------------------------------------------------------------- /components/header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { signIn, signOut } from "next-auth/react"; 3 | import styles from "./header.module.css"; 4 | import { SessionContext } from "../utils/session"; 5 | import { useContext } from "react"; 6 | import appConfig from "../app.config"; 7 | 8 | /** 9 | * I did not make any significant changes to this code so it may not work 10 | */ 11 | export default function Header() { 12 | const session = useContext(SessionContext); 13 | 14 | return ( 15 |
16 | 19 |
20 |

21 | {!session?.user && ( 22 | <> 23 | 24 | You are not signed in 25 | 26 | 27 | )} 28 | {session?.user && ( 29 | <> 30 | {session.user.image && ( 31 | 35 | )} 36 | 37 | Signed in as 38 |
39 | {session.user.email ?? session.user.name} 40 |
41 | { 45 | e.preventDefault(); 46 | signOut(); 47 | }} 48 | > 49 | Sign out 50 | 51 | 52 | )} 53 |

54 |
55 | 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /components/layout.tsx: -------------------------------------------------------------------------------- 1 | import Header from "./header"; 2 | import Footer from "./footer"; 3 | import type { ReactNode } from "react"; 4 | import NetworkStatus from "./network-status"; 5 | 6 | export default function Layout({ children }: { children: ReactNode }) { 7 | return ( 8 | <> 9 | 10 |
11 |
{children}
12 |