Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions docs/OAUTH_LOGOUT_SETUP.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# OAuth Logout Configuration for Ministry Platform

## Current Status
βœ… Basic sign-out implemented using NextAuth server action
⚠️ OIDC RP-initiated logout not yet configured
βœ… Basic sign-out implemented using Better Auth server action
βœ… OIDC RP-initiated logout configured

## What's Working
- Application-level session termination via `signOut()` server action
- NextAuth clears local session cookies
- Better Auth clears local session cookies
- User is logged out of the Next.js application
- OIDC RP-initiated logout ends Ministry Platform OAuth session

## What's Missing (For Complete OIDC Logout)

Expand All @@ -30,7 +31,7 @@ http://localhost:3000/signin

#### OIDC RP-Initiated Logout Flow:
When a user clicks "Sign out", the current implementation:
1. βœ… Destroys NextAuth session (JWT)
1. βœ… Destroys Better Auth session (JWT)
2. ❌ Does NOT notify Ministry Platform to end the OAuth session

#### To implement full logout:
Expand Down Expand Up @@ -67,17 +68,17 @@ export async function handleSignOut() {
// Get current session to extract id_token
const session = await auth();
const idToken = session?.idToken;

const params = new URLSearchParams({
post_logout_redirect_uri: process.env.NEXTAUTH_URL || 'http://localhost:3000',
post_logout_redirect_uri: process.env.BETTER_AUTH_URL || process.env.NEXTAUTH_URL || 'http://localhost:3000',
});

if (idToken) {
params.append('id_token_hint', idToken);
}

// Sign out of NextAuth first
await signOut({
// Sign out of Better Auth first
await signOut({
redirect: false // Don't redirect yet
});

Expand All @@ -86,8 +87,8 @@ export async function handleSignOut() {
}
```

#### Option B: Simple logout (Current Implementation)
Current implementation only logs out of NextAuth locally. This is acceptable if:
#### Option B: Simple logout
Local-only logout of Better Auth. This is acceptable if:
- You don't need to clear Ministry Platform session
- Users are okay with auto-login on next visit (SSO behavior)

Expand All @@ -96,8 +97,8 @@ Ensure these are set:

```env
MINISTRY_PLATFORM_BASE_URL=https://your-mp-instance.com
NEXTAUTH_URL=https://yourdomain.com # Production
NEXTAUTH_URL=http://localhost:3000 # Development
BETTER_AUTH_URL=https://yourdomain.com # Production
BETTER_AUTH_URL=http://localhost:3000 # Development
```

### 5. Testing
Expand All @@ -117,7 +118,7 @@ NEXTAUTH_URL=http://localhost:3000 # Development

## References
- [OpenID Connect RP-Initiated Logout Spec](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)
- [NextAuth.js v5 Logout Documentation](https://authjs.dev/guides/basics/signout)
- [Better Auth Documentation](https://www.better-auth.com/docs)

## Decision Required

Expand All @@ -133,4 +134,4 @@ Choose implementation based on your requirements:
- Cons: SSO session remains, users auto-login
- Use when: SSO convenience is preferred

Currently implemented: **Option B**
Currently implemented: **Option A (Full OIDC Logout)**
47 changes: 9 additions & 38 deletions scripts/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,38 +115,20 @@ const ENV_VARS: EnvVar[] = [
description: 'Ministry Platform API base URL',
},
{
name: 'NEXTAUTH_SECRET',
name: 'BETTER_AUTH_SECRET',
required: true,
sensitive: true,
description: 'NextAuth encryption secret',
description: 'Better Auth encryption secret (fallback: NEXTAUTH_SECRET)',
autoGenerate: true,
},
{
name: 'NEXTAUTH_URL',
name: 'BETTER_AUTH_URL',
required: true,
sensitive: false,
description: 'Application URL',
description: 'Application URL (fallback: NEXTAUTH_URL)',
defaultValue: 'http://localhost:3000',
},
// Optional variables
{
name: 'OIDC_PROVIDER_NAME',
required: false,
sensitive: false,
description: 'OAuth provider display name',
},
{
name: 'OIDC_SCOPE',
required: false,
sensitive: false,
description: 'OAuth scopes',
},
{
name: 'OIDC_WELL_KNOWN_URL',
required: false,
sensitive: false,
description: 'OIDC well-known configuration URL',
},
{
name: 'NEXT_PUBLIC_MINISTRY_PLATFORM_FILE_URL',
required: false,
Expand All @@ -159,12 +141,6 @@ const ENV_VARS: EnvVar[] = [
sensitive: false,
description: 'Application display name',
},
{
name: 'NEXTAUTH_DEBUG',
required: false,
sensitive: false,
description: 'Enable NextAuth debug logging',
},
];

// ============================================================================
Expand Down Expand Up @@ -395,7 +371,7 @@ function printResult(result: StepResult): void {
}
}

async function generateNextAuthSecret(): Promise<string> {
async function generateAuthSecret(): Promise<string> {
// Generate a random secret using Node.js crypto
const { randomBytes } = await import('node:crypto');
return randomBytes(32).toString('base64');
Expand All @@ -412,13 +388,11 @@ function normalizeMPHost(input: string): string {

function deriveMPUrls(host: string): {
baseUrl: string;
wellKnownUrl: string;
fileUrl: string;
} {
const normalizedHost = normalizeMPHost(host);
return {
baseUrl: `https://${normalizedHost}/ministryplatformapi`,
wellKnownUrl: `https://${normalizedHost}/ministryplatformapi/oauth/.well-known/openid-configuration`,
fileUrl: `https://${normalizedHost}/ministryplatformapi/files`,
};
}
Expand Down Expand Up @@ -849,7 +823,7 @@ async function runInteractiveSetup(options: SetupOptions): Promise<number> {
console.log(chalk.bold.blue('\nMPNext Setup'));
console.log(chalk.blue('============'));

const totalSteps = 10;
const totalSteps = 9;
let passedSteps = 0;
let warnings = 0;
let failedSteps = 0;
Expand Down Expand Up @@ -998,7 +972,6 @@ async function runInteractiveSetup(options: SetupOptions): Promise<number> {
// Variables that are auto-derived from the MP host
const mpDerivedVars = [
'MINISTRY_PLATFORM_BASE_URL',
'OIDC_WELL_KNOWN_URL',
'NEXT_PUBLIC_MINISTRY_PLATFORM_FILE_URL',
];

Expand Down Expand Up @@ -1027,11 +1000,9 @@ async function runInteractiveSetup(options: SetupOptions): Promise<number> {
if (mpHost) {
const derived = deriveMPUrls(mpHost);
updates.set('MINISTRY_PLATFORM_BASE_URL', derived.baseUrl);
updates.set('OIDC_WELL_KNOWN_URL', derived.wellKnownUrl);
updates.set('NEXT_PUBLIC_MINISTRY_PLATFORM_FILE_URL', derived.fileUrl);

console.log(chalk.green(` βœ“ MINISTRY_PLATFORM_BASE_URL = ${derived.baseUrl}`));
console.log(chalk.green(` βœ“ OIDC_WELL_KNOWN_URL = ${derived.wellKnownUrl}`));
console.log(chalk.green(` βœ“ NEXT_PUBLIC_MINISTRY_PLATFORM_FILE_URL = ${derived.fileUrl}`));
}

Expand Down Expand Up @@ -1110,14 +1081,14 @@ async function runInteractiveSetup(options: SetupOptions): Promise<number> {

console.log(chalk.yellow(`\n ${varDef.name}: ${varDef.description}`));

if (varDef.autoGenerate && varDef.name === 'NEXTAUTH_SECRET') {
if (varDef.autoGenerate && varDef.name === 'BETTER_AUTH_SECRET') {
const shouldGenerate = await confirm({
message: `Auto-generate ${varDef.name}?`,
default: true,
});

if (shouldGenerate) {
const secret = await generateNextAuthSecret();
const secret = await generateAuthSecret();
updates.set(varDef.name, secret);
console.log(chalk.green(` βœ“ Generated ${varDef.name}`));
} else {
Expand Down Expand Up @@ -1263,7 +1234,7 @@ async function runInteractiveSetup(options: SetupOptions): Promise<number> {
failedSteps++;
}

// Step 9: Summary
// Summary
console.log(chalk.bold.blue('\n\nSetup Complete!'));
console.log(chalk.blue('==============='));

Expand Down
6 changes: 3 additions & 3 deletions src/lib/providers/ministry-platform/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ ministry-platform/
β”œβ”€β”€ provider.ts # Main provider class
β”œβ”€β”€ helper.ts # Public API helper
β”œβ”€β”€ auth/ # Authentication
β”‚ β”œβ”€β”€ auth-provider.ts # NextAuth provider
β”‚ β”œβ”€β”€ client-credentials.ts # OAuth client credentials
β”‚ └── types.ts # Auth-related types
β”‚ β”œβ”€β”€ types.ts # Auth-related types
β”‚ └── index.ts # Barrel export
β”œβ”€β”€ services/ # Service layer
β”‚ β”œβ”€β”€ table.service.ts
β”‚ β”œβ”€β”€ procedure.service.ts
Expand Down Expand Up @@ -87,7 +87,7 @@ MINISTRY_PLATFORM_CLIENT_SECRET=your_client_secret
- βœ… Automatic OAuth2 token management
- βœ… Service-oriented architecture
- βœ… Zod schema validation
- βœ… NextAuth integration
- βœ… Better Auth integration
- βœ… File upload/download support
- βœ… Comprehensive error handling
- βœ… Clean, standards-compliant code organization
Expand Down
4 changes: 2 additions & 2 deletions src/test-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import { vi } from 'vitest';
vi.stubEnv('MINISTRY_PLATFORM_BASE_URL', 'https://test-mp.example.com');
vi.stubEnv('OIDC_CLIENT_ID', 'test-client-id');
vi.stubEnv('OIDC_CLIENT_SECRET', 'test-client-secret');
vi.stubEnv('NEXTAUTH_SECRET', 'test-secret-key-for-testing');
vi.stubEnv('NEXTAUTH_URL', 'http://localhost:3000');
vi.stubEnv('BETTER_AUTH_SECRET', 'test-secret-key-for-testing');
vi.stubEnv('BETTER_AUTH_URL', 'http://localhost:3000');
vi.stubEnv('NODE_ENV', 'test');