In this article, we will give you a quick overview of OAuth 2.0 authentication and walk you through the steps needed to integrate “Sign In With Privacy Portal” with your web application.
OAUTH 2.0 Code Flow
OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user’s resources on another server, like social media accounts or email, without sharing their credentials. It uses tokens for security and is widely adopted for API access control.
Privacy Portal is an OAuth 2.0 provider focused on user privacy. It offers the usual OAuth 2.0 functionality but aims to keep the identity of users private by hiding their Personally Identifiable Information, such as their email address.
Step 1 - Register your OAuth App
- Go to the Privacy Portal App.
- Create you free account (in case you don’t already have one).
- Open to Developer Settings.
- Tap on “New Application” to register your OAuth Application.
- Fill in the information requested then tag on “Register”.
Note that the “Callback URL” is the URL that Privacy Portal will redirect back to after the authorization step is complete.
Step 2 - Get your App Credentials
- Go to Developer Settings.
- Select the OAuth Application you just created.
- Under “Credentials”, copy your Applications’s
client_id
. - Also under “Credentials”, tap on “Generate Secret” and copy the
client_secret
.
Note that the client_secret
will only be displayed to you once. Make sure to treat it as a password and store it securely.
Step 3 - Create the Login button
In your application’s client code, create the login button that redirects to Privacy Portal’s Authentication URL.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OAuth Login with Privacy Portal</title>
<style>
.login-button {
background-color: #000;
color: white;
border: none;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
</style>
<script>
document.getElementById('pp-login').addEventListener('click', function () {
// Your Privacy Portal OAuth client ID
const clientId = 'YOUR_CLIENT_ID_FROM_PRIVACY_PORTAL';
// Redirect URI should match what you set in the OAuth App settings
const redirectUri = 'YOUR_REDIRECT_URI';
// Define the permissions you need
const scope = 'openid name email';
// Generate a random state
const state = generateRandomState();
// Store the state in local storage
localStorage.setItem('oauth_state', state);
// Construct the OAuth URL
const oauthUrl = new URL('https://app.privacyportal.org/oauth/authorize');
oauthUrl.searchParams.append('client_id', clientId);
oauthUrl.searchParams.append('redirect_uri', redirectUri);
oauthUrl.searchParams.append('scope', scope);
oauthUrl.searchParams.append('response_type', 'code');
oauthUrl.searchParams.append('state', state);
// Open the OAuth URL in a new window or tab
window.location.href = oauthUrl;
});
function generateRandomState(length = 16) {
// Generate an array of random integers
const randomValues = new Uint8Array(length);
crypto.getRandomValues(randomValues);
// Convert the random values to a hexadecimal string
return Array.from(randomValues, (byte) => byte.toString(16).padStart(2, '0')).join('');
}
</script>
</head>
<body>
<button id="pp-login" class="login-button">Login with Privacy Portal</button>
</body>
</html>
Step 4 - Handle the Redirect URI
In your application’s client code, create a new route matching the Redirect URI. After authorizing users, the OAuth provider will redirect back to your application using the redirect_uri
. During the redirection, the provider will pass some parameters as part of the request.
Your application should handle the Redirect URI route by validating the OAuth params, and if valid, it should initiate the login request with your backend server.
// Function to handle OAuth2 callback
function handleOAuthCallback() {
// Extract parameters from URL
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
// Retrieve the state from local storage
const storedState = localStorage.getItem('oauth_state');
// State validation
if (!state || state !== storedState) {
console.error('State validation failed. Possible CSRF attack.');
// You might want to redirect to an error page or log out the user
return;
}
// Clear the state from local storage for next use
localStorage.removeItem('oauth_state');
// If we've got this far, the state is valid
if (code) {
// Here you would typically send the authorization code to your backend
// to exchange for an access token
fetch('https://your-backend-endpoint/oauth/authenticate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
code,
redirect_uri
})
})
.then((response) => response.json())
.then((data) => {
// Handle the token response from your server
console.log('Token received:', data);
// You might want to set this token in cookies, local storage, or however your app manages sessions
// For example:
// localStorage.setItem('access_token', data.access_token);
// Now, redirect or update UI to indicate logged in state
})
.catch((error) => {
console.error('Failed to fetch token:', error);
});
} else {
console.error('No authorization code received.');
}
}
// Call this function when the page loads
window.onload = handleOAuthCallback;
Step 5 - Handle the OAuth Login Request
In the previous step, your client application calls your backend server in order to start the authentication process within your application. Your backend server should handle this OAuth authentication request and log the user into your application.
Here’s a simplified example using a NodeJS / ExpressJS backend handling the OAuth Login Request:
const express = require('express');
const fetch = require('node-fetch');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
// Your Privacy Portal OAuth Client ID
const CLIENT_ID = 'your-client-id';
// Your Privacy Portal OAuth Client Secret (must be treated like a password)
const CLIENT_SECRET = process.env.CLIENT_SECRET;
// Your OAuth Application's Redirect URI (this could be passed by the client)
const REDIRECT_URI = 'your-redirect-uri';
// The Privacy Portal OAuth Token Endpoint
// API docs: https://privacyportal.org/developers/api-docs#oauth_token
const TOKEN_ENDPOINT = 'https://api.privacyportal.org/oauth/token';
// The Privacy Portal User Info Endpoint
// API docs: https://privacyportal.org/developers/api-docs#openid_userinfo
const USERINFO_ENDPOINT = 'https://api.privacyportal.org/oauth/userinfo';
// Handle the OAuth Login Request
app.post('/oauth/authenticate', async (req, res) => {
try {
const { code } = req.body;
// Prepare the URLSearchParams for the body
const body = new URLSearchParams();
body.append('code', code);
body.append('client_id', CLIENT_ID);
body.append('client_secret', CLIENT_SECRET);
body.append('redirect_uri', REDIRECT_URI);
body.append('grant_type', 'authorization_code');
// Exchange authorization code for access token
const tokenResponse = await fetch(TOKEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: body
});
const tokenData = await tokenResponse.json();
// Assuming the access token is in tokenData.access_token
const accessToken = tokenData.access_token;
// Fetch user info using the access token
const userInfoResponse = await fetch(USERINFO_ENDPOINT, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
const user = await userInfoResponse.json();
// Here you would typically process the user data,
// e.g., save to your database, create a session, etc.
// To keep this example simple, let's assume a function exists to
// create a JWT token from user data
const token = createUserSession(user);
res.json({
message: 'Login successful',
token
});
} catch (error) {
console.error('Error during OAuth callback:', error);
res.status(500).json({ message: 'An error occurred during authentication' });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Please note that the code snippets provided on this page are not production-ready and require multiple additional steps before they can be used in your production application. Some of these steps include things such as input validation, error handling, and user management.
Additional Steps
In this tutorial, we covered the base OAuth 2.0 Code Flow. One recommended security improvement to the example above would be using Proof Key for Code Exchange (or PKCE).
PKCE allows you to protect the authorization code
from being intercepted and used by a malicious actor. With PKCE, your application’s client generates a secret called code_verifier
, it sends its hashed value as a code_challenge
to the OAuth provider during authorization, then uses it in combination with the authorization code
to login.
Supporting Proof Key for Code Exchange
To support PKCE, few changes are required:
- Generate the
code_verifier
on the client.
// Function to generate the PKCE code_verifier
function generateCodeVerifier(length = 64) {
// Generate random bytes (minimum length 32 bytes)
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
// Convert the array to a Base64 URL-safe string
return base64URLEncode(array);
}
function base64URLEncode(buffer) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)))
.replace(///g, '_').replace(/+/g, '-').replace(/=+$/, '');
}
// generate the code_verifier
const code_verifier = generateCodeVerifier();
// Store the code_verifier as part of the oauth_state in the local storage
- Create the PKCE
code_challenge
by hashing thecode_verifier
// Function to create the code_challenge by hashing the code_verifier
async function createCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await window.crypto.subtle.digest('SHA-256', data);
return base64URLEncode(digest);
}
// create the code_challenge
const code_challenge = await createCodeChallenge(code_verifier);
// set the code_challenge method to S256 since we're using SHA-256
const code_challenge_method = 'S256';
- Add the
code_challenge
andcode_challenge_method
params to the Authorization URI redirection. - When handling the redirection, read the stored
code_verifier
and add it to the login request. - Add the
code_verifier
param to the Issue Access Token request.