Skip to content
Open
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
76 changes: 68 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/mikestaub/passport-atprotocol/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/passport-atprotocol.svg?style=flat)](https://www.npmjs.com/package/passport-atprotocol) [![Coverage Status](https://coveralls.io/repos/github/mikestaub/passport-atprotocol/badge.svg?branch=main)](https://coveralls.io/github/mikestaub/passport-atprotocol?branch=main) [![Discord](https://img.shields.io/discord/1097580399187738645?style=flat&logo=discord&logoColor=white)](https://discord.gg/tCD8MMfq)

## WARNING: this library is currently in development and should not be used in production
## This library is now production-ready with proper database storage, error handling, and logging

## Quickstart

Expand Down Expand Up @@ -228,13 +228,73 @@ Generally they should be rotated every 90 days for security
npm run rotate-keys
```

## Production Considerations
## Production Usage

- use a database to store session and user data
- use NodeOAuthClientOptions.requestLock=true if running multiple server instances
- implement proper key rotation
- implement proper token revocation
- implement proper error handling
- implement proper logging
For production use, it's recommended to:

1. Use a database to store session and user data:

```javascript
const { RedisStateStore, RedisSessionStore } = require('./storage-implementations/redis-store');

const stateStore = new RedisStateStore({ url: 'redis://localhost:6379' });
const sessionStore = new RedisSessionStore({ url: 'redis://localhost:6379' });

const oauthClient = createOAuthClient({
clientMetadata,
keyset,
stateStore,
sessionStore,
requestLock: true // Important for multiple server instances
});
```

2. Configure proper logging:

```javascript
const { ConsoleLogger } = require('passport-atprotocol');

const logger = new ConsoleLogger({ level: 'debug' });

const oauthClient = createOAuthClient({
clientMetadata,
keyset,
stateStore,
sessionStore,
logger,
requestLock: true
});
```

3. Implement proper key rotation:

Keys should be rotated every 90 days for security. Use the provided script:

```
npm run rotate-keys
```

4. Implement proper token revocation:

```javascript
app.get('/auth/atprotocol/revoke', ensureAuthenticated, (req, res) => {
oauthClient
.revoke(req.user.profile.did)
.then(() => {
req.logout((err) => {
if (err) {
logger.error('Logout error:', err);
}
res.redirect('/');
});
})
.catch((error) => {
logger.error('Failed to revoke token:', error);
res.status(500).send('Failed to revoke token: ' + error.message);
});
});
```

Example implementations for Redis and MongoDB storage are provided in the `example/storage-implementations` directory.


16 changes: 15 additions & 1 deletion example/passport/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const {
createOAuthClient,
createATProtocolLoginMiddleware,
ATprotocolStrategy,
ConsoleLogger,
InMemoryStateStore,
InMemorySessionStore,
} = require('../../dist/index');

const app = express();
Expand Down Expand Up @@ -56,8 +59,19 @@ setupKeys()
dpop_bound_access_tokens: true,
};

const stateStore = new InMemoryStateStore();
const sessionStore = new InMemorySessionStore();
const logger = new ConsoleLogger({ level: 'debug' });

// Note: this client uses memory storage, in production consider providing your own implementation
const oauthClient = createOAuthClient({ clientMetadata, keyset });
const oauthClient = createOAuthClient({
clientMetadata,
keyset,
stateStore,
sessionStore,
logger,
requestLock: false // Set to true in production with multiple instances
});

const strategy = new ATprotocolStrategy(
{
Expand Down
100 changes: 100 additions & 0 deletions example/storage-implementations/mongo-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { StateStore, SessionStore } from '../../src/storage';
import { MongoClient, Collection } from 'mongodb';

/**
* MongoDB state store implementation
*/
export class MongoStateStore implements StateStore {
private client: MongoClient;
private collection: Collection;

constructor(options: { url: string; dbName: string; collectionName?: string }) {
this.client = new MongoClient(options.url);
const db = this.client.db(options.dbName);
this.collection = db.collection(options.collectionName || 'atproto_states');
this.connect();
}

private async connect() {
await this.client.connect();
await this.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 });
}

async set(key: string, internalState: any): Promise<void> {
await this.collection.updateOne(
{ _id: key },
{
$set: {
...internalState,
createdAt: new Date()
}
},
{ upsert: true }
);
}

async get(key: string): Promise<any | undefined> {
const result = await this.collection.findOne({ _id: key });
if (!result) return undefined;

const { _id, createdAt, ...state } = result;
return state;
}

async del(key: string): Promise<void> {
await this.collection.deleteOne({ _id: key });
}

async close(): Promise<void> {
await this.client.close();
}
}

/**
* MongoDB session store implementation
*/
export class MongoSessionStore implements SessionStore {
private client: MongoClient;
private collection: Collection;

constructor(options: { url: string; dbName: string; collectionName?: string }) {
this.client = new MongoClient(options.url);
const db = this.client.db(options.dbName);
this.collection = db.collection(options.collectionName || 'atproto_sessions');
this.connect();
}

private async connect() {
await this.client.connect();
await this.collection.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 86400 * 30 });
}

async set(sub: string, sessionData: any): Promise<void> {
await this.collection.updateOne(
{ _id: sub },
{
$set: {
...sessionData,
updatedAt: new Date()
}
},
{ upsert: true }
);
}

async get(sub: string): Promise<any | undefined> {
const result = await this.collection.findOne({ _id: sub });
if (!result) return undefined;

const { _id, updatedAt, ...sessionData } = result;
return sessionData;
}

async del(sub: string): Promise<void> {
await this.collection.deleteOne({ _id: sub });
}

async close(): Promise<void> {
await this.client.close();
}
}
74 changes: 74 additions & 0 deletions example/storage-implementations/redis-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { StateStore, SessionStore } from '../../src/storage';
import { createClient } from 'redis';

/**
* Redis state store implementation
*/
export class RedisStateStore implements StateStore {
private client;
private prefix: string;

constructor(options: { url: string; prefix?: string }) {
this.client = createClient({ url: options.url });
this.prefix = options.prefix || 'atproto:state:';
this.client.connect();
}

async set(key: string, internalState: any): Promise<void> {
await this.client.set(
`${this.prefix}${key}`,
JSON.stringify(internalState),
{ EX: 3600 } // 1 hour expiration
);
}

async get(key: string): Promise<any | undefined> {
const data = await this.client.get(`${this.prefix}${key}`);
if (!data) return undefined;
return JSON.parse(data);
}

async del(key: string): Promise<void> {
await this.client.del(`${this.prefix}${key}`);
}

async close(): Promise<void> {
await this.client.quit();
}
}

/**
* Redis session store implementation
*/
export class RedisSessionStore implements SessionStore {
private client;
private prefix: string;

constructor(options: { url: string; prefix?: string }) {
this.client = createClient({ url: options.url });
this.prefix = options.prefix || 'atproto:session:';
this.client.connect();
}

async set(sub: string, sessionData: any): Promise<void> {
await this.client.set(
`${this.prefix}${sub}`,
JSON.stringify(sessionData),
{ EX: 86400 * 30 } // 30 days expiration
);
}

async get(sub: string): Promise<any | undefined> {
const data = await this.client.get(`${this.prefix}${sub}`);
if (!data) return undefined;
return JSON.parse(data);
}

async del(sub: string): Promise<void> {
await this.client.del(`${this.prefix}${sub}`);
}

async close(): Promise<void> {
await this.client.quit();
}
}
Loading