Gilos Auth for Developers
Allow users to sign in to your application using their Gilos ID. Gilos Auth implements the OAuth 2.0 Authorization Code flow.
Sign in to see interactive examples with your actual OAuth clients.
https://auth.gilos.org
Quick Start
Integrating Gilos Auth takes just 3 steps:
- Create a client - Register your app at Gilos Auth
- Redirect to authorize - Send users to our authorization page
- Exchange code for token - Get an access token and fetch user data
Exchange the authorization code for an access token (server-side only):
Request
POST https://auth.gilos.org/o/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&code=AUTHORIZATION_CODE
&redirect_uri=https://yourapp.com/callback
cURL Command
curl -X POST https://auth.gilos.org/o/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "code=AUTHORIZATION_CODE" \
-d "redirect_uri=https://yourapp.com/callback"
Response
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "xyz789...",
"scope": "profile email"
}
Fetch the authenticated user's information:
Request
GET https://auth.gilos.org/o/me
Authorization: Bearer YOUR_ACCESS_TOKEN
cURL Command
curl https://auth.gilos.org/o/me \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response
{
"id": 12345,
"username": "johndoe",
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com"
}
Only fields matching your requested scopes will be returned.
Refresh Token
Access tokens expire after 1 hour. Use the refresh token to get a new one:
curl -X POST https://auth.gilos.org/o/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "refresh_token=YOUR_REFRESH_TOKEN"
Scopes
Scopes define what user data your application can access:
| Scope | Description | Returns |
|---|---|---|
profile | User's profile info (name, username, etc.) | first_name, last_name, username |
email | User's email address | email |
phone | User's phone number | phone |
Error Handling
API errors return JSON with an error field:
{
"error": "invalid_grant",
"message": "Invalid or expired authorization code"
}
| Error | Description |
|---|---|
invalid_client | Client ID or secret is incorrect |
invalid_grant | Authorization code or refresh token is invalid/expired |
invalid_request | Missing required parameters |
access_denied | User denied the authorization request |
Example: Go (Golang)
This is a complete, production-ready example in Go using the standard library. It implements the latest OAuth 2.1 specifications including PKCE (Proof Key for Code Exchange) and state verification for CSRF protection.
package gilos
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
// Config holds the OAuth configuration for Gilos Auth
type Config struct {
ClientID string
ClientSecret string
RedirectURI string
Scopes string
AuthURL string
TokenURL string
UserInfoURL string
}
// DefaultConfig returns the default configuration for Gilos Auth.
var DefaultConfig = Config{
ClientID: os.Getenv("GILOS_CLIENT_ID"),
ClientSecret: os.Getenv("GILOS_CLIENT_SECRET"),
RedirectURI: "https://yourapp.com/callback",
Scopes: "profile email",
AuthURL: "https://auth.gilos.org/o/authorize",
TokenURL: "https://auth.gilos.org/o/token",
UserInfoURL: "https://auth.gilos.org/o/me",
}
// cfg holds the active configuration
var cfg = DefaultConfig
// httpClient is a shared HTTP client with timeouts
var httpClient = &http.Client{Timeout: 10 * time.Second}
// Configure sets a custom configuration
func Configure(c Config) {
cfg = c
}
// Init initializes the configuration from environment variables
func Init() {
cfg.ClientID = os.Getenv("GILOS_CLIENT_ID")
cfg.ClientSecret = os.Getenv("GILOS_CLIENT_SECRET")
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope"`
}
type UserInfoResponse struct {
ID int64 `json:"id"`
Username string `json:"username"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
Email string `json:"email,omitempty"`
}
// generateState creates a secure random state string
func generateState() (string, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
// generatePKCE creates a code verifier and its S256 challenge
func generatePKCE() (verifier, challenge string, err error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", "", err
}
verifier = base64.RawURLEncoding.EncodeToString(b)
hash := sha256.Sum256([]byte(verifier))
challenge = base64.RawURLEncoding.EncodeToString(hash[:])
return verifier, challenge, nil
}
// ExchangeCode exchanges an authorization code for tokens using PKCE
func ExchangeCode(code, codeVerifier string) (*TokenResponse, error) {
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("code", code)
data.Set("redirect_uri", cfg.RedirectURI)
data.Set("client_id", cfg.ClientID)
data.Set("client_secret", cfg.ClientSecret)
data.Set("code_verifier", codeVerifier) // PKCE (OAuth 2.1)
resp, err := httpClient.Post(cfg.TokenURL, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
if err != nil {
return nil, fmt.Errorf("failed to exchange code: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("token exchange failed: %s - %s", resp.Status, string(body))
}
var tokenResp TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return nil, fmt.Errorf("failed to parse token response: %w", err)
}
return &tokenResp, nil
}
// RefreshToken refreshes an access token using a refresh token
func RefreshToken(refreshToken string) (*TokenResponse, error) {
data := url.Values{}
data.Set("grant_type", "refresh_token")
data.Set("refresh_token", refreshToken)
data.Set("client_id", cfg.ClientID)
data.Set("client_secret", cfg.ClientSecret)
resp, err := httpClient.Post(cfg.TokenURL, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
if err != nil {
return nil, fmt.Errorf("failed to refresh token: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("token refresh failed: %s - %s", resp.Status, string(body))
}
var tokenResp TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return nil, fmt.Errorf("failed to parse refresh response: %w", err)
}
return &tokenResp, nil
}
// GetUserInfo fetches user information from Gilos Auth
func GetUserInfo(accessToken string) (*UserInfoResponse, error) {
req, err := http.NewRequest("GET", cfg.UserInfoURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("user info request failed: %s - %s", resp.Status, string(body))
}
var userInfo UserInfoResponse
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
return nil, err
}
return &userInfo, nil
}
type AuthCallback func(w http.ResponseWriter, r *http.Request, userInfo *UserInfoResponse, tokens *TokenResponse) (redirectURL string, err error)
// AuthorizeHandler initiates the OAuth flow
func AuthorizeHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
state, _ := generateState()
verifier, challenge, _ := generatePKCE()
// Store state and verifier in secure, short-lived cookies
cookieOpts := &http.Cookie{
Path: "/",
MaxAge: 600, // 10 minutes
HttpOnly: true,
Secure: r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https",
SameSite: http.SameSiteLaxMode,
}
stateCookie := *cookieOpts
stateCookie.Name = "oauth_state"
stateCookie.Value = state
http.SetCookie(w, &stateCookie)
verifierCookie := *cookieOpts
verifierCookie.Name = "oauth_verifier"
verifierCookie.Value = verifier
http.SetCookie(w, &verifierCookie)
params := url.Values{}
params.Set("response_type", "code")
params.Set("client_id", cfg.ClientID)
params.Set("redirect_uri", cfg.RedirectURI)
params.Set("scope", cfg.Scopes)
params.Set("state", state)
params.Set("code_challenge", challenge) // PKCE (OAuth 2.1)
params.Set("code_challenge_method", "S256") // Required by OAuth 2.1
redirectURL := cfg.AuthURL + "?" + params.Encode()
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
}
}
// CallbackHandler processes the authorization redirect
func CallbackHandler(onSuccess AuthCallback) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := r.URL.Query().Get("error"); err != "" {
http.Error(w, "Authorization denied: "+err, http.StatusForbidden)
return
}
// Verify state parameter
stateCookie, err := r.Cookie("oauth_state")
if err != nil || stateCookie.Value != r.URL.Query().Get("state") {
http.Error(w, "Invalid state parameter", http.StatusForbidden)
return
}
// Retrieve PKCE code verifier
verifierCookie, err := r.Cookie("oauth_verifier")
if err != nil {
http.Error(w, "Missing PKCE verifier", http.StatusBadRequest)
return
}
// Clear security cookies immediately
clearCookie := &http.Cookie{
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https",
SameSite: http.SameSiteLaxMode,
}
stateClear := *clearCookie
stateClear.Name = "oauth_state"
http.SetCookie(w, &stateClear)
verifierClear := *clearCookie
verifierClear.Name = "oauth_verifier"
http.SetCookie(w, &verifierClear)
code := r.URL.Query().Get("code")
if code == "" {
http.Error(w, "Authorization code missing", http.StatusBadRequest)
return
}
// Exchange code for tokens
tokens, err := ExchangeCode(code, verifierCookie.Value)
if err != nil {
http.Error(w, "Failed to exchange code: "+err.Error(), http.StatusInternalServerError)
return
}
// Fetch user info
userInfo, err := GetUserInfo(tokens.AccessToken)
if err != nil {
http.Error(w, "Failed to fetch user info: "+err.Error(), http.StatusInternalServerError)
return
}
// Call application callback
redirectURL, err := onSuccess(w, r, userInfo, tokens)
if err != nil {
http.Error(w, "Authentication handler failed", http.StatusInternalServerError)
return
}
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
}
}