Google SSO - Integration
A writeup on integrating Google SSO with Captiveportal.
Google SSO - Integration
A writeup on integrating Google SSO with Captiveportal.
Date: September 18, 2025
Categories: Programming, Linux
Tags: linux, cheatsheet, terminal
π Overview
This project integrates Google Single Sign-On (SSO) authentication into a WiFi captive portal controlled by CoovaChilli and FreeRADIUS.
It allows users to access the Internet using:
- β Google Workspace login (OAuth2)
- β Username & Password (RADIUS)
- β Coupon/Voucher login
After successful authentication, users are logged into CoovaChilli and redirected to a branded success page.
π§ Components Used
- CoovaChilli β Captive portal / access controller
- FreeRADIUS β AAA server (Authentication, Authorization, Accounting)
- MySQL (MariaDB) β RADIUS database
- PHP (daloRADIUS based pages) β Custom login / SSO integration
- Google OAuth 2.0 β Identity provider for authentication
π Directory Layout
1
2
3
4
5
6
7
8
9
10
/var/www/html/hotspotlogin/
β
βββ config.php # Centralized configuration
βββ login-choice.php # Landing page for method selection
βββ google-sso.php # Initiates Google OAuth
βββ sso-callback.php # Handles Google response, updates RADIUS
βββ hotspotlogin.php # Regular username/password or coupon login
βββ success.php # Final success page
βββ logoff.php # Handles logout
βββ assets/ # (Optional) images, icons, CSS
βοΈ Configuration File
config.php
This file stores all deployment-specific settings β Google credentials, CoovaChilli IPs, DB credentials, and domain/group mappings.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
return [
// ===== GOOGLE OAUTH CONFIG =====
'google_client_id' => 'YOUR_CLIENT_ID.apps.googleusercontent.com',
'google_client_secret' => 'YOUR_GOOGLE_CLIENT_SECRET',
'google_redirect_uri' => 'https://yourdomain.com/hotspotlogin/sso-callback.php',
// ===== COOVACHILLI CONFIG =====
'uam_secret' => 'testing123', // Shared secret between portal & CoovaChilli
'uam_ip' => '172.27.0.1',
'uam_port' => '3990',
// ===== RADIUS DATABASE CONFIG =====
'db_host' => 'localhost',
'db_name' => 'radius',
'db_user' => 'radiususer',
'db_pass' => 'radiuspass',
// ===== DOMAIN β RADIUS GROUP MAPPING =====
'allowed_domains' => [
'amyntortech.com' => 'google_sso_students',
'clientdomain.com' => 'google_sso_staff'
],
];
When deploying to a new client:
- Replace Google Client ID, Secret, and Redirect URI
- Update domain mappings
- Update RADIUS DB credentials and UAM IP/port
π§ Login Flow
πΉ Step 1 β User Chooses Login Method
File: login-choice.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
if (session_status() === PHP_SESSION_NONE) session_start();
// Store CoovaChilli parameters
$params = ['uamip','uamport','challenge','called','mac','ip','nasid','sessionid','userurl','res'];
foreach ($params as $p) $_SESSION[$p] = $_GET[$p] ?? $_SESSION[$p] ?? '';
$uamip = $_SESSION['uamip'] ?: '172.27.0.1';
$uamport = $_SESSION['uamport'] ?: '3990';
$session_params = http_build_query([
'uamip' => $uamip, 'uamport' => $uamport,
'challenge' => $_SESSION['challenge'] ?? '',
'mac' => $_SESSION['mac'] ?? '',
'ip' => $_SESSION['ip'] ?? '',
'nasid' => $_SESSION['nasid'] ?? '',
'sessionid' => $_SESSION['sessionid'] ?? '',
'res' => 'notyet'
]);
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WiFi Login</title>
<style>
body { font-family: sans-serif; background: linear-gradient(135deg,#667eea,#764ba2);
min-height:100vh;display:flex;justify-content:center;align-items:center;}
.container { background:#fff; border-radius:16px; padding:48px; max-width:420px; width:100%;
box-shadow:0 20px 60px rgba(0,0,0,0.3);}
.login-btn { display:flex; align-items:center; justify-content:center; gap:12px; padding:16px;
border-radius:8px; text-decoration:none; margin-bottom:12px; font-weight:600;}
.google-btn { background:white; color:#111; border:1px solid #ddd;}
.credential-btn { background:#667eea; color:white;}
.coupon-btn { background:#10b981; color:white;}
</style>
</head>
<body>
<div class="container">
<h2 style="text-align:center;">Welcome</h2>
<p style="text-align:center;">Choose your login method</p>
<a href="google-sso.php?<?= $session_params ?>" class="login-btn google-btn">Continue with Google</a>
<a href="hotspotlogin.php?<?= $session_params ?>" class="login-btn credential-btn">Username & Password</a>
<a href="hotspotlogin.php?<?= $session_params ?>&login-type=coupon" class="login-btn coupon-btn">Coupon Code</a>
</div>
</body>
</html>
πΉ Step 2 β Start Google OAuth
File: google-sso.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
if (session_status() === PHP_SESSION_NONE) session_start();
$config = include('config.php');
$client_id = $config['google_client_id'];
$redirect = $config['google_redirect_uri'];
$scope = 'openid email profile';
$auth_url = 'https://accounts.google.com/o/oauth2/v2/auth?' . http_build_query([
'client_id' => $client_id,
'redirect_uri' => $redirect,
'response_type' => 'code',
'scope' => $scope,
'access_type' => 'offline',
'prompt' => 'select_account',
]);
header("Location: $auth_url");
exit;
πΉ Step 3 β Handle Callback
File: sso-callback.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?php
if (session_status() === PHP_SESSION_NONE) session_start();
$config = include('config.php');
$client_id = $config['google_client_id'];
$client_secret = $config['google_client_secret'];
$redirect_uri = $config['google_redirect_uri'];
// Get auth code
if (!isset($_GET['code'])) die("Authorization code missing.");
// Exchange code for tokens
$ch = curl_init('https://oauth2.googleapis.com/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => [
'code' => $_GET['code'],
'client_id' => $client_id,
'client_secret' => $client_secret,
'redirect_uri' => $redirect_uri,
'grant_type' => 'authorization_code',
],
]);
$response = curl_exec($ch);
$tokens = json_decode($response, true);
curl_close($ch);
if (!isset($tokens['id_token'])) die("No ID token received.");
// Decode JWT payload
$parts = explode('.', $tokens['id_token']);
$payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true);
$email = $payload['email'] ?? '';
$firstname = $payload['given_name'] ?? strtok($email, '@');
$lastname = $payload['family_name'] ?? '';
if (!$email) die("Email missing in ID token.");
// Connect to RADIUS DB
$db = new mysqli($config['db_host'], $config['db_user'], $config['db_pass'], $config['db_name']);
if ($db->connect_error) die("DB connection failed: " . $db->connect_error);
$domain = substr(strrchr($email, "@"), 1);
$group = $config['allowed_domains'][$domain] ?? 'google_sso_default';
// Insert/update user in radcheck
$db->query("REPLACE INTO radcheck (username, attribute, op, value)
VALUES ('$email', 'Cleartext-Password', ':=', 'googlepass')");
// Map group
$db->query("REPLACE INTO radusergroup (username, groupname, priority)
VALUES ('$email', '$group', 1)");
$_SESSION['sso_login_user'] = $email;
// Redirect to CoovaChilli login
$uamip = $_SESSION['uamip'] ?? $config['uam_ip'];
$uamport = $_SESSION['uamport'] ?? $config['uam_port'];
$userurl = urlencode("https://{$_SERVER['HTTP_HOST']}/hotspotlogin/success.php?username=$email");
header("Location: http://$uamip:$uamport/login?username=$email&password=googlepass&userurl=$userurl");
exit;
πΉ Step 4 β Success Page
File: success.php
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if (session_status() === PHP_SESSION_NONE) session_start();
$username = $_GET['username'] ?? $_SESSION['sso_login_user'] ?? 'Guest';
?>
<!doctype html>
<html>
<head><meta charset="utf-8"><title>Login Successful</title></head>
<body style="font-family:sans-serif;text-align:center;margin-top:50px;">
<h2>Welcome, <?= htmlspecialchars($username) ?>!</h2>
<p>You are now connected to the Internet.</p>
<a href="logoff.php">Logout</a>
</body>
</html>
πΉ Step 5 β Logoff
File: logoff.php
1
2
3
4
5
6
7
8
9
10
<?php
if (session_status() === PHP_SESSION_NONE) session_start();
$config = include('config.php');
$uamip = $_GET['uamip'] ?? $config['uam_ip'];
$uamport = $_GET['uamport'] ?? $config['uam_port'];
session_destroy();
header("Location: http://$uamip:$uamport/logoff");
exit;
π Deployment Steps for a New Client
- Copy
/hotspotlogin/folder to new server - Create
config.phpand update:- Google client credentials
- Redirect URI
- DB credentials
- Domain/group mapping
- CoovaChilli UAM IP/port
- Set Google OAuth redirect URI in Google Cloud Console:
https://yourdomain.com/hotspotlogin/sso-callback.php - Restart Apache and CoovaChilli
- Test login via captive portal:
http://172.27.0.1:3990
π Security Tips
- Always use HTTPS for the portal domain
- Restrict Google OAuth to your Workspace domain via the
hdparameter - Use a strong
uam_secret - Keep Google client secrets out of version control
π§© Debugging
Check logs:
1
2
tail -f /var/log/apache2/error.log
tail -f /var/log/freeradius/radius.log
β Summary of Editable Files
| File | Edit Needed | Purpose |
|---|---|---|
| config.php | β Yes | Core configuration |
| login-choice.php | Optional | UI customization |
| google-sso.php | No (reads config) | Starts Google login |
| sso-callback.php | No (reads config) | Processes Google login |
| success.php | Optional | Branding |
| logoff.php | No | Handles logout |
π― User Flow Summary
- User connects β redirected to
login-choice.php - Chooses βLogin with Googleβ
- Google authenticates β returns to
sso-callback.php - User added to RADIUS DB β redirected to CoovaChilli login
- Coova grants access β
success.php - User can log off via
logoff.php
π₯οΈ Example Redirects
| Stage | URL Example |
|---|---|
| Google Login Start | /hotspotlogin/google-sso.php |
| OAuth Callback | /hotspotlogin/sso-callback.php?code=... |
| Coova Login | http://172.27.0.1:3990/login?username=... |
| Success | /hotspotlogin/success.php?username=... |
| Logoff | /hotspotlogin/logoff.php |
π Network Flow Summary
Client β CoovaChilli β Captive Portal β Google OAuth β CoovaChilli β Internet
1
2
3
4
5
6
7
8
9
10
11
12
13
User Device
β
CoovaChilli Gateway
β (Redirect)
hotspotlogin/login-choice.php
β
Google OAuth (google-sso.php β sso-callback.php)
β
FreeRADIUS DB Update
β
CoovaChilli Login (grants IP access)
β
success.php β Internet access
π§ Notes
- Google Workspace admins must preapprove your redirect URI
- If captive portal uses HTTP only, Chrome may block Google OAuth β always use HTTPS
- βAuthorization code already usedβ errors are normal if users refresh the callback URL
βοΈ Author
Implementation & integration by HacktheProtocol Infosec team.
This document explains the Google SSO captive portal deployment step by step.
