OAuth 2.0 with GitLab
OAuth 2.0 authentication using GitLab as identity provider.
Overview
The platform uses GitLab OAuth 2.0 for user authentication: - GitLab as IdP: Single sign-on via GitLab.com or self-hosted GitLab - Authorization Code Flow: Standard OAuth 2.0 flow with PKCE - Group-based RBAC: Automatic role assignment based on GitLab groups - Token exchange: Exchange GitLab token for platform JWT - API integration: Access GitLab API on behalf of users
Architecture
sequenceDiagram
participant User
participant App
participant Auth Service
participant GitLab
User->>App: Click "Login with GitLab"
App->>Auth Service: Initiate OAuth
Auth Service->>GitLab: Authorization Request
GitLab->>User: Login Page
User->>GitLab: Authenticate
GitLab->>Auth Service: Authorization Code
Auth Service->>GitLab: Exchange Code for Token
GitLab->>Auth Service: GitLab Access Token
Auth Service->>App: Platform JWT
App->>User: Authenticated
OAuth Flow
1. Initiate Authorization
GET /api/v1/auth/oauth/authorize?provider=gitlab
Redirect to GitLab:
https://gitlab.bluefly.io/oauth/authorize
?client_id=abc123
&redirect_uri=https://llm.bluefly.io/auth/callback
&response_type=code
&scope=read_user+read_api+read_repository
&state=random-state-string
&code_challenge=challenge-string
&code_challenge_method=S256
Query Parameters:
- client_id: OAuth app client ID
- redirect_uri: Callback URL (must match registered URI)
- response_type: Always code for authorization code flow
- scope: Requested permissions (space-separated)
- state: CSRF protection token
- code_challenge: PKCE code challenge
- code_challenge_method: PKCE method (S256 for SHA-256)
2. User Authorizes
User logs into GitLab and authorizes the application.
3. Authorization Callback
GitLab redirects back with authorization code:
https://llm.bluefly.io/auth/callback
?code=authorization-code-abc123
&state=random-state-string
4. Exchange Code for Token
POST /api/v1/auth/oauth/token
Content-Type: application/json
Request:
{
"grant_type": "authorization_code",
"code": "authorization-code-abc123",
"redirect_uri": "https://llm.bluefly.io/auth/callback",
"client_id": "abc123",
"code_verifier": "verifier-string"
}
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read write admin",
"user": {
"id": "user-123",
"username": "developer",
"email": "developer@bluefly.io",
"name": "John Developer",
"avatar_url": "https://gitlab.bluefly.io/uploads/...",
"gitlab_id": 456,
"roles": ["developer", "agent-admin"],
"groups": [
{
"id": 789,
"name": "llm-platform",
"role": "maintainer"
}
]
}
}
OAuth Scopes
Available Scopes
| Scope | Description | Permission Level |
|---|---|---|
read_user |
Read user profile | User info only |
read_api |
Read GitLab API | Read-only access |
write_api |
Write GitLab API | Full API access |
read_repository |
Read repositories | Clone, pull |
write_repository |
Write repositories | Push, create |
sudo |
Admin operations | Admin only |
api |
Full API access | All operations |
Recommended Scopes
Read-only User:
read_user read_api
Developer:
read_user read_api read_repository write_repository
Admin:
read_user api
Group-Based RBAC
Roles are automatically assigned based on GitLab group membership:
Group to Role Mapping
{
"groupMappings": [
{
"gitlabGroup": "llm-platform/admins",
"platformRole": "admin",
"permissions": ["*"]
},
{
"gitlabGroup": "llm-platform/developers",
"platformRole": "developer",
"permissions": [
"agent:execute",
"workflow:create",
"mesh:communicate"
]
},
{
"gitlabGroup": "llm-platform/users",
"platformRole": "user",
"permissions": [
"agent:read",
"workflow:read"
]
}
]
}
Role Inheritance
Users inherit permissions from all groups they belong to:
Example:
{
"user": "developer@bluefly.io",
"groups": [
"llm-platform/developers",
"llm-platform/ml-team"
],
"roles": ["developer", "ml-engineer"],
"permissions": [
"agent:execute",
"workflow:create",
"mesh:communicate",
"model:train",
"model:deploy"
]
}
GitLab API Access
Access GitLab API on behalf of authenticated user:
Get User's GitLab Token
GET /api/v1/auth/oauth/gitlab-token
Authorization: Bearer eyJhbGci...
Response:
{
"access_token": "gitlab-token-xyz789",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "gitlab-refresh-abc123",
"created_at": 1705320000,
"scope": "read_user read_api"
}
Use GitLab Token
curl -H "Authorization: Bearer gitlab-token-xyz789" \
https://gitlab.bluefly.io/api/v4/user
Response:
{
"id": 456,
"username": "developer",
"email": "developer@bluefly.io",
"name": "John Developer",
"state": "active",
"avatar_url": "https://gitlab.bluefly.io/uploads/...",
"web_url": "https://gitlab.bluefly.io/developer"
}
Token Refresh
Refresh expired GitLab token:
POST /api/v1/auth/oauth/refresh
Content-Type: application/json
Request:
{
"grant_type": "refresh_token",
"refresh_token": "gitlab-refresh-abc123",
"provider": "gitlab"
}
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"gitlab_token": {
"access_token": "gitlab-token-new123",
"refresh_token": "gitlab-refresh-new456",
"expires_in": 7200
}
}
PKCE (Proof Key for Code Exchange)
PKCE prevents authorization code interception attacks:
1. Generate Code Verifier
JavaScript:
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64URLEncode(array);
}
2. Generate Code Challenge
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64URLEncode(new Uint8Array(hash));
}
3. Store Code Verifier
Store verifier in session storage for callback:
sessionStorage.setItem('code_verifier', codeVerifier);
4. Send Challenge in Authorization
https://gitlab.bluefly.io/oauth/authorize
?code_challenge=BASE64_ENCODED_CHALLENGE
&code_challenge_method=S256
...
5. Send Verifier in Token Exchange
{
"code_verifier": "original-code-verifier",
...
}
Self-Hosted GitLab
Configure custom GitLab instance:
Update OAuth Configuration
POST /api/v1/auth/oauth/configure
Authorization: Bearer admin-token
Content-Type: application/json
Request:
{
"provider": "gitlab-custom",
"config": {
"authorizationURL": "https://gitlab.yourcompany.com/oauth/authorize",
"tokenURL": "https://gitlab.yourcompany.com/oauth/token",
"userInfoURL": "https://gitlab.yourcompany.com/api/v4/user",
"clientID": "your-client-id",
"clientSecret": "your-client-secret",
"callbackURL": "https://llm.bluefly.io/auth/callback/gitlab-custom"
}
}
Security Considerations
State Parameter
Always validate state parameter to prevent CSRF:
const state = generateRandomString();
sessionStorage.setItem('oauth_state', state);
// Later, in callback:
const returnedState = new URLSearchParams(window.location.search).get('state');
if (returnedState !== sessionStorage.getItem('oauth_state')) {
throw new Error('Invalid state parameter - possible CSRF attack');
}
Redirect URI Validation
Only allow whitelisted redirect URIs:
Allowed:
- https://llm.bluefly.io/auth/callback
- http://localhost:3000/auth/callback (development only)
Blocked:
- https://evil.com/steal-token
Token Storage
Store tokens securely:
✅ Recommended: - HTTP-only secure cookies - Server-side session storage - Encrypted browser storage
❌ Avoid: - LocalStorage (XSS vulnerable) - URL parameters - Plain cookies
Integration Examples
React Application
import { useAuth } from '@bluefly/auth-react';
function LoginButton() {
const { loginWithGitLab } = useAuth();
const handleLogin = async () => {
const user = await loginWithGitLab({
scope: 'read_user read_api',
redirectUri: window.location.origin + '/auth/callback'
});
console.log('Logged in as:', user.name);
};
return <button onClick={handleLogin}>Login with GitLab</button>;
}
Node.js Backend
import { OAuthClient } from '@bluefly/auth-server';
const oauth = new OAuthClient({
provider: 'gitlab',
clientId: process.env.GITLAB_CLIENT_ID,
clientSecret: process.env.GITLAB_CLIENT_SECRET,
redirectUri: 'https://llm.bluefly.io/auth/callback'
});
app.get('/auth/gitlab', (req, res) => {
const authUrl = oauth.getAuthorizationUrl({
scope: 'read_user read_api',
state: req.session.state
});
res.redirect(authUrl);
});
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
if (state !== req.session.state) {
return res.status(403).send('Invalid state');
}
const tokens = await oauth.exchangeCodeForToken({
code,
codeVerifier: req.session.codeVerifier
});
req.session.user = tokens.user;
res.redirect('/dashboard');
});
Troubleshooting
Common Errors
Error: invalid_grant
- Code expired (10 minutes max)
- Code already used
- Code verifier mismatch
Error: redirect_uri_mismatch
- Callback URL doesn't match registered URI
- Protocol mismatch (http vs https)
- Port mismatch
Error: access_denied
- User denied authorization
- User lacks required GitLab permissions
Debug Mode
Enable OAuth debug logging:
export OAUTH_DEBUG=true
export OAUTH_LOG_LEVEL=debug
Logs will include: - Authorization URLs - Token exchange requests - User profile data - Group memberships - Role assignments