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.

Try it live!

Sign in to see interactive examples with your actual OAuth clients.

Base URL

https://auth.gilos.org

Quick Start

Integrating Gilos Auth takes just 3 steps:

  1. Create a client - Register your app at Gilos Auth
  2. Redirect to authorize - Send users to our authorization page
  3. Exchange code for token - Get an access token and fetch user data

OAuth2 Flow

1 Authorization

Redirect users to the Gilos Auth authorization page:

GET /o/authorize

Query Parameters

ParameterRequiredDescription
client_idYesYour application's Client ID
redirect_uriYesMust exactly match your registered callback URL
response_typeYesMust be code
scopeYesSpace-separated list of scopes

Example Authorization URL

https://auth.gilos.org/o/authorize?
  client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/callback
  &response_type=code
  &scope=profile email

After the user approves, they'll be redirected to your callback URL with an authorization code:

https://yourapp.com/callback?code=abc123def456...
2 Exchange Code for Token

Exchange the authorization code for an access token (server-side only):

POST /o/token

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"
}
3 Get User Info

Fetch the authenticated user's information:

GET /o/me

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"
}
Scope-based response

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:

POST /o/token
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:

ScopeDescriptionReturns
profileUser's profile info (name, username, etc.)first_name, last_name, username
emailUser's email addressemail
phoneUser's phone numberphone

Error Handling

API errors return JSON with an error field:

{
  "error": "invalid_grant",
  "message": "Invalid or expired authorization code"
}
ErrorDescription
invalid_clientClient ID or secret is incorrect
invalid_grantAuthorization code or refresh token is invalid/expired
invalid_requestMissing required parameters
access_deniedUser 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)
	}
}