Automatically authenticates through a FortiGate captive portal on every network connect — no browser, no manual login, no interruptions.
Built for IIT BHU's network, but works on any FortiGate-based university or enterprise captive portal.
Replace the link above with your Google Drive direct download link.
To get a direct download link from Google Drive: right-click file → Share → Copy link, then change/viewto/uc?export=downloadin the URL.
FortiGate captive portals use a three-step handshake to authenticate a device:
Step 1: Device makes any HTTP request (e.g. detectportal.firefox.com)
└─> FortiGate transparently injects its login page HTML into the response
containing a one-time magic token: /fgtauth?<TOKEN>
Step 2: Browser GETs /fgtauth?<TOKEN>
└─> FortiGate initializes a server-side session tied to that token
Step 3: Browser POSTs credentials + token to the portal root
└─> FortiGate validates, registers the device MAC, grants internet access
This script replicates all three steps programmatically using curl (Linux) or Invoke-WebRequest (Windows), then verifies the connection succeeded.
Key behaviours:
- The magic token is single-use and session-specific — a new one is generated every time you connect. This is expected and correct.
- FortiGate abruptly closes the TCP connection after receiving valid credentials. This is normal firmware behaviour, not a failure.
- Credentials are saved locally after the first run and never prompted again.
- Windows 10 or later
- PowerShell 5.1+ (pre-installed on all modern Windows)
- Download
FortiAuth.exefrom the link above - Double-click it — you will be prompted for credentials once
- Done — it registers itself and runs silently on every WiFi connect from now on
To reset credentials (e.g. password change):
del %APPDATA%\FortiAuth\creds.txtThen run FortiAuth.exe again.
Install ps2exe — run once in PowerShell as Administrator:
Install-Module ps2exe -Scope CurrentUser -Forcecd "path\to\FortiAuth"
Invoke-PS2EXE `
-InputFile ".\FortiAuth.ps1" `
-OutputFile ".\FortiAuth.exe" `
-NoConsole:$false `
-Title "IIT BHU FortiGate Auth" `
-Version "1.0.0" `
-requireAdminThis produces a standalone FortiAuth.exe with no external dependencies.
| Step | What happens |
|---|---|
| First launch | Prompts for credentials, saves them to %APPDATA%\FortiAuth\creds.txt, copies itself to AppData, registers a Task Scheduler event triggered on network connect |
| Every subsequent WiFi connect | Task Scheduler fires the .exe silently in the background |
| Portal detection | Fetches detectportal.firefox.com, scans the response body for fgtauth?<TOKEN> pattern using regex |
| Session init | GETs the fgtauth URL to register the session on FortiGate's side |
| Authentication | POSTs URL-encoded credentials + token to the portal root |
| Verification | Re-fetches the test URL and confirms the body no longer contains an fgtauth redirect |
curlpython3(for URL encoding)NetworkManagermanaging your WiFi (standard on Ubuntu, Arch, Fedora, openSUSE)
chmod +x fortiauth.sh
./fortiauth.shOn first run it will:
- Prompt for your credentials (stored at
~/.config/fortiauth/credswithchmod 600) - Install itself to
/etc/NetworkManager/dispatcher.d/99-fortiauth - Authenticate immediately
From that point on it runs automatically and silently every time any network interface comes up.
rm ~/.config/fortiauth/creds
bash fortiauth.shsudo /etc/NetworkManager/dispatcher.d/99-fortiauth eth0 up| Step | What happens |
|---|---|
| First launch | Saves credentials, copies script to NetworkManager dispatcher directory |
| Every network-up event | NetworkManager calls the script automatically with $2 = "up" |
| Any other event | Script exits immediately — $2 != "up" guard at the top |
| Portal detection | curl fetches test URL, grep -oE extracts fgtauth?TOKEN from body |
| Fallback | If not found in body, probes 192.168.249.1:1000 directly |
| Session init | curl GETs the fgtauth URL silently to initialize server-side session |
| Authentication | curl POSTs credentials; connection drop is ignored with || true |
| Verification | Re-fetches test URL and confirms body is free of fgtauth pattern |
Note: If your server uses
systemd-networkdinstead of NetworkManager, the dispatcher hook will not fire. Use a systemd service unit instead — open an issue and I will add one.
Both scripts share these two variables near the top. Edit before compiling (Windows) or before first run (Linux):
| Variable | Default | Description |
|---|---|---|
PORTAL_IP |
192.168.249.1:1000 |
FortiGate portal address — change if your institution differs |
TEST_URL |
detectportal.firefox.com/canonical.html |
Used to detect portal and verify internet after auth |
| Situation | Behaviour |
|---|---|
| No network at all | Exits cleanly with Network unreachable |
| Already authenticated | Detects no portal, exits with Already authenticated |
| Different network (cafe, home wifi) | No fgtauth token found in body, exits cleanly — does nothing |
| Wrong password saved | Verification step fails, prints reset instructions |
| FortiGate drops TCP on success | Handled — connection close is caught and ignored, verification proceeds normally |
| Token expires between steps | Rare on any normal machine; re-running gets a fresh token automatically |
- Credentials are stored in plaintext on disk (
creds.txton Windows,credson Linux). - File permissions are restricted to the current user only (
chmod 600on Linux). - This is a reasonable tradeoff for a university captive portal — these are not banking credentials.
- If plaintext storage is a concern, the credential file section can be replaced with a secrets manager such as
libsecret/ GNOME Keyring on Linux, or Windows Credential Manager on Windows.
Windows:
schtasks /Delete /TN "IITBHUFortiAuth" /F
rmdir /S /Q "%APPDATA%\FortiAuth"Linux:
sudo rm /etc/NetworkManager/dispatcher.d/99-fortiauth
rm -rf ~/.config/fortiauth| Platform | Version |
|---|---|
| Windows | 11 (10.0.26200) |
| Network | IIT BHU FortiGate captive portal |
Pull requests welcome. If your institution uses a different FortiGate firmware version with a different token format or endpoint, open an issue with the output of:
curl -v http://detectportal.firefox.com/canonical.html 2>&1 | head -50MIT