A complete, production-ready example of Sentry OAuth 2.0 authentication with beautiful UI and comprehensive documentation.
This project demonstrates exactly how to integrate Sentry authentication into your application, with clear explanations of each step in the OAuth flow.
This implementation follows the OAuth 2.0 Authorization Code flow with PKCE (Proof Key for Code Exchange) security. The authentication process involves three main components:
- Frontend (React) - Handles user interaction and authentication state
- Backend (Express.js) - Manages OAuth flow and session storage
- Sentry OAuth Provider - Authenticates users and provides access tokens
sequenceDiagram
participant U as User
participant F as Frontend
participant B as Backend
participant S as Sentry OAuth
U->>F: Click "Login with Sentry"
F->>B: GET /api/auth/login
B->>B: Generate CSRF state
B->>F: Return authorization URL
F->>U: Redirect to Sentry OAuth
U->>S: Enter credentials
S->>B: GET /api/auth/callback?code=...&state=...
B->>B: Validate state parameter
B->>S: POST /oauth/token/ (exchange code)
S->>B: Return access token + user data
B->>B: Store user in database
B->>B: Create session
B->>F: Redirect to /auth/success
F->>B: GET /api/auth/me
B->>F: Return user data
F->>U: Show Dashboard
- β Authorization URL Generation - How to create Sentry OAuth URLs with proper scopes
- β State Parameter Validation - CSRF protection for OAuth requests
- β Token Exchange - Converting authorization codes to access tokens
- β User Data Retrieval - Getting user information from Sentry's API
- β Session Management - Maintaining authenticated user state
- π Required Scopes:
org:read project:read team:read member:read event:read
for comprehensive read access - π OAuth Endpoints:
- Authorization:
https://sentry.io/oauth/authorize/
- Token Exchange:
https://sentry.io/oauth/token/
- User Info:
https://sentry.io/api/0/user/
- Authorization:
- π€ User Information: ID, email, name, username, avatar from Sentry
- π§ Configuration: OAuth app setup in Sentry organization settings
- π‘οΈ Security: State validation, secure sessions, token encryption
- π Production-Ready OAuth 2.0 - Complete Sentry authentication flow
- π¨ Beautiful Modern UI - Professional login page with animations
- π± Responsive Design - Works perfectly on all devices
- ποΈ Simple Storage - In-memory user storage (easy to understand)
- π Secure Sessions - HTTPOnly cookie-based authentication
- π Comprehensive Logging - See exactly what happens during OAuth
- β‘ One-Command Setup - Get running in seconds
-
Setup Project:
npm run setup
-
Configure Sentry OAuth:
- Edit
server/.env
with your Sentry OAuth credentials - Get credentials from your Sentry organization settings
- Edit
-
Start Development:
npm run dev
-
Open Browser:
- Frontend: http://localhost:5173
- Backend: http://localhost:3001
Add these to server/.env
:
SENTRY_OAUTH_CLIENT_ID=your_client_id
SENTRY_OAUTH_CLIENT_SECRET=your_client_secret
SENTRY_OAUTH_REDIRECT_URI=http://localhost:3001/api/auth/callback
SESSION_SECRET=your-random-secret-key
npm run dev # Start both frontend and backend
npm run dev:backend # Backend only (port 3001)
npm run dev:frontend # Frontend only (port 5173)
npm run build # Build for production
npm run start # Start production mode
graph TB
subgraph "Frontend (React)"
A[App.tsx] --> B[AuthProvider]
B --> C[LoginPage]
B --> D[Dashboard]
B --> E[AuthSuccess]
B --> F[AuthError]
C --> G[useAuth Hook]
D --> G
E --> G
F --> G
end
subgraph "Backend (Express.js)"
H[index.js] --> I[SentryOAuthService]
H --> J[Database]
H --> K[Session Management]
I --> L[OAuth Routes]
L --> M[/api/auth/login]
L --> N[/api/auth/callback]
L --> O[/api/auth/me]
L --> P[/api/auth/logout]
end
subgraph "External"
Q[Sentry OAuth Server]
end
G -.->|HTTP Requests| L
I -.->|OAuth Flow| Q
flowchart LR
subgraph "Authentication State Management"
A[User State] --> B[AuthContext]
B --> C[Session Storage]
C --> D[Database]
end
subgraph "OAuth Flow"
E[Authorization URL] --> F[Sentry OAuth]
F --> G[Authorization Code]
G --> H[Access Token]
H --> I[User Information]
end
subgraph "Security Layer"
J[CSRF State] --> K[Session Validation]
K --> L[Cookie Security]
L --> M[Token Encryption]
end
I --> A
K --> B
sentry-auth-example/
βββ server/ # Express.js backend
β βββ services/
β β βββ sentry-oauth.js # OAuth service implementation
β β βββ sentry-api.js # Sentry API integration
β βββ database.js # User storage layer
β βββ index.js # Express server & routes
β βββ .env.example # Environment template
βββ sentryauth-frontend/ # React frontend
β βββ src/
β β βββ contexts/
β β β βββ AuthContext.tsx # Global auth state
β β βββ components/
β β β βββ LoginPage.tsx # Login UI
β β β βββ Dashboard.tsx # Protected dashboard
β β β βββ AuthSuccess.tsx # OAuth success handler
β β β βββ AuthError.tsx # OAuth error handler
β β β βββ SentryMetrics.tsx # Sentry data display
β β βββ App.tsx # Main app component
β β βββ main.tsx # App entry point
β βββ .env.example
βββ package.json # Root package with scripts
// Global authentication state management
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Main authentication functions:
const checkAuth = async () => {
// Validates current session with backend
// Called on app initialization and after OAuth success
const response = await fetch('/api/auth/me', { credentials: 'include' });
if (response.ok) setUser(data.user);
};
const login = async () => {
// Initiates OAuth flow by getting authorization URL
const response = await fetch('/api/auth/login', { credentials: 'include' });
const data = await response.json();
window.location.href = data.authUrl; // Redirect to Sentry
};
const logout = async () => {
// Destroys server session and clears client state
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
setUser(null);
};
AuthSuccess Component (/src/components/AuthSuccess.tsx
):
// Handles successful OAuth redirect from Sentry
useEffect(() => {
const refreshAndRedirect = async () => {
await checkAuth(); // Refresh auth state
setTimeout(() => window.location.href = '/', 1000); // Redirect to dashboard
};
refreshAndRedirect();
}, []);
AuthError Component (/src/components/AuthError.tsx
):
// Handles OAuth errors with retry functionality
const urlParams = new URLSearchParams(window.location.search);
const errorMessage = urlParams.get('message'); // Extract error from URL
class SentryOAuthService {
// Step 1: Generate authorization URL with CSRF protection
getAuthorizationUrl(state) {
const params = new URLSearchParams({
response_type: 'code',
client_id: this.config.clientId,
redirect_uri: this.config.redirectUri,
scope: 'org:read project:read team:read member:read event:read', // Sentry read scopes
state: state // CSRF protection
});
return `${this.config.baseUrl}/oauth/authorize/?${params.toString()}`;
}
// Step 2: Exchange authorization code for access token
async exchangeCodeForToken(code) {
const response = await fetch(`${this.config.baseUrl}/oauth/token/`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
code: code,
redirect_uri: this.config.redirectUri
})
});
return response.json();
}
// Step 3: Get user information from Sentry API
async getUserInfo(accessToken) {
// Try multiple endpoints for user data
const endpoints = [
'/oauth/userinfo', '/api/0/user/', '/api/0/users/me/'
];
for (const endpoint of endpoints) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (response.ok) return response.json();
}
}
}
// Middleware: Require authentication for protected routes
const requireAuth = (req, res, next) => {
if (req.session && req.session.userId) {
next();
} else {
res.status(401).json({ error: 'Authentication required' });
}
};
// Route: Initiate OAuth flow
app.get('/api/auth/login', (req, res) => {
const state = uuidv4(); // Generate CSRF token
req.session.oauthState = state; // Store in session
const authUrl = sentryOAuthService.getAuthorizationUrl(state);
res.json({ authUrl });
});
// Route: Handle OAuth callback
app.get('/api/auth/callback', async (req, res) => {
const { code, state } = req.query;
// Validate CSRF state
if (state !== req.session.oauthState) {
return res.status(400).json({ error: 'Invalid state parameter' });
}
// Complete OAuth flow
const { user, accessToken } = await sentryOAuthService.completeOAuthFlow(code);
// Store user and create session
const dbUser = await database.createUser(user, accessToken);
req.session.userId = dbUser.id;
// Redirect to success page
res.redirect(`${frontendUrl}/auth/success`);
});
// Route: Get current user
app.get('/api/auth/me', requireAuth, async (req, res) => {
const user = await database.findUserById(req.session.userId);
res.json({ user: user });
});
// Route: Logout
app.post('/api/auth/logout', (req, res) => {
req.session.destroy((err) => {
if (err) return res.status(500).json({ error: 'Failed to logout' });
res.clearCookie('connect.sid');
res.json({ message: 'Logged out successfully' });
});
});
class SimpleUserStore {
// Create new user from Sentry OAuth data
createUser(sentryUser, accessToken) {
const user = {
id: this.nextId++,
sentry_id: sentryUser.id,
email: sentryUser.email,
name: sentryUser.name,
username: sentryUser.username,
avatar_url: sentryUser.avatar,
access_token: accessToken, // Store for API calls
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
// Store in multiple indexes for fast lookup
this.users.set(user.id, user);
this.usersBySentryId.set(user.sentry_id, user);
this.usersByEmail.set(user.email, user);
return user;
}
// Update existing user on subsequent logins
updateUser(sentryUser, accessToken) {
const existingUser = this.usersBySentryId.get(sentryUser.id);
const updatedUser = {
...existingUser,
email: sentryUser.email, // Update with fresh data
name: sentryUser.name,
username: sentryUser.username,
avatar_url: sentryUser.avatar,
access_token: accessToken, // Refresh token
updated_at: new Date().toISOString()
};
// Update all indexes
this.users.set(updatedUser.id, updatedUser);
this.usersBySentryId.set(updatedUser.sentry_id, updatedUser);
return updatedUser;
}
}
- OAuth 2.0 Authorization Code Flow - Industry standard secure authentication
- CSRF Protection - State parameter validation prevents cross-site request forgery
- Secure Session Management - HTTPOnly cookies with encryption
- Token Storage - Access tokens encrypted and stored securely
- CORS Configuration - Proper cross-origin resource sharing setup
- HTTPS Ready - Production configuration for secure connections
- Secure Headers - Security headers for production deployment
- SQL Injection Prevention - Parameterized queries and input validation
- Session Encryption - Sessions encrypted with secure secret keys
- Sensitive Data Handling - Access tokens never exposed to frontend
- Comprehensive Error Handling - Secure error messages without data leakage
- Go to your Sentry organization settings
- Navigate to Developer Settings β OAuth Applications
- Click Create New Application
- Configure:
- Name: Your app name
- Homepage URL:
http://localhost:5173
(development) - Authorization callback URL:
http://localhost:3001/api/auth/callback
- Save the
Client ID
andClient Secret
Create server/.env
:
SENTRY_OAUTH_CLIENT_ID=your_actual_client_id
SENTRY_OAUTH_CLIENT_SECRET=your_actual_client_secret
SENTRY_OAUTH_REDIRECT_URI=http://localhost:3001/api/auth/callback
SESSION_SECRET=your-super-secret-session-key-at-least-32-chars
SENTRY_BASE_URL=https://sentry.io
stateDiagram-v2
[*] --> Unauthenticated
Unauthenticated --> GeneratingAuthURL : User clicks login
note right of GeneratingAuthURL : Backend generates CSRF state<br/>Creates authorization URL
GeneratingAuthURL --> RedirectingToSentry : Return auth URL to frontend
note right of RedirectingToSentry : Frontend redirects browser<br/>to Sentry OAuth page
RedirectingToSentry --> SentryAuth : User at Sentry login
note right of SentryAuth : User enters credentials<br/>Sentry validates user
SentryAuth --> ValidatingCallback : Sentry redirects with code
note right of ValidatingCallback : Backend validates CSRF state<br/>Exchanges code for token
ValidatingCallback --> CreatingSession : Token exchange successful
note right of CreatingSession : Store user in database<br/>Create server session
CreatingSession --> AuthSuccess : Redirect to success page
note right of AuthSuccess : Frontend refreshes auth state<br/>Redirects to dashboard
AuthSuccess --> Authenticated : User logged in
Authenticated --> Unauthenticated : User logs out
ValidatingCallback --> AuthError : OAuth error occurred
AuthError --> Unauthenticated : User retries or goes home
flowchart TD
A[OAuth Callback] --> B{State Valid?}
B -->|No| C[CSRF Error]
B -->|Yes| D{Code Present?}
D -->|No| E[Missing Code Error]
D -->|Yes| F[Exchange Code for Token]
F --> G{Token Exchange Success?}
G -->|No| H[Token Exchange Error]
G -->|Yes| I[Get User Info]
I --> J{User Info Success?}
J -->|No| K[User Info Error]
J -->|Yes| L[Store User]
L --> M{Database Success?}
M -->|No| N[Database Error]
M -->|Yes| O[Create Session]
O --> P{Session Success?}
P -->|No| Q[Session Error]
P -->|Yes| R[Redirect to Success]
C --> S[Redirect to Error Page]
E --> S
H --> S
K --> S
N --> S
Q --> S
R --> T[Frontend Success Handler]
S --> U[Frontend Error Handler]
T --> V[Refresh Auth State]
V --> W[Redirect to Dashboard]
U --> X[Show Error Message]
X --> Y[Retry or Go Home]
sequenceDiagram
participant Browser
participant Frontend
participant Backend
participant Database
Note over Browser,Database: Session Lifecycle
Browser->>Frontend: App loads
Frontend->>Backend: GET /api/auth/me
Backend->>Backend: Check session cookie
alt Session exists
Backend->>Database: Find user by session.userId
Database->>Backend: Return user data
Backend->>Frontend: User data
Frontend->>Browser: Show Dashboard
else No session
Backend->>Frontend: 401 Unauthorized
Frontend->>Browser: Show Login Page
end
Note over Browser,Database: Login Flow
Browser->>Frontend: Click Login
Frontend->>Backend: GET /api/auth/login
Backend->>Backend: Generate state + session
Backend->>Frontend: Authorization URL
Frontend->>Browser: Redirect to Sentry
Browser->>Backend: OAuth callback
Backend->>Backend: Validate state
Backend->>Database: Store/update user
Backend->>Backend: Create session
Backend->>Browser: Redirect to success
Note over Browser,Database: Logout Flow
Browser->>Frontend: Click Logout
Frontend->>Backend: POST /api/auth/logout
Backend->>Backend: Destroy session
Backend->>Backend: Clear cookies
Backend->>Frontend: Success response
Frontend->>Browser: Show Login Page
- Use strong, random
SESSION_SECRET
(32+ characters) - Set
NODE_ENV=production
- Use HTTPS URLs for redirect URIs
- Configure proper CORS origins
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
}));
- Replace in-memory storage with persistent database
- Implement proper user schema with indexes
- Add database connection pooling
- Set up database backups
- Add structured logging with Winston or Bunyan
- Implement health check endpoints
- Set up error tracking (ironically, with Sentry!)
- Add performance monitoring
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
ISC License - see LICENSE file for details