Buku Tamu SSO

Python (Flask)

Contoh integrasi OAuth dengan Flask.

# oauth_client.py
import os
import secrets
import hashlib
import base64
from urllib.parse import urlencode
from flask import Flask, redirect, request, session, jsonify
import requests

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)

# Konfigurasi
CONFIG = {
    'client_id': 'YOUR_CLIENT_ID',
    'client_secret': 'YOUR_CLIENT_SECRET',  # Hanya untuk confidential client
    'redirect_uri': 'http://localhost:5000/callback',
    # Origin saja — path /api/auth/oauth2/... ditambahkan di bawah.
    # Issuer OIDC (untuk validasi iss) = origin + "/api/auth"
    'auth_server_url': 'https://bukutamu.tabalongkab.go.id',
    'issuer': 'https://bukutamu.tabalongkab.go.id/api/auth',
    'scopes': ['openid', 'profile', 'email', 'role', 'organization'],
}


def generate_code_verifier() -> str:
    """Generate random code verifier untuk PKCE"""
    return secrets.token_urlsafe(64)[:64]


def generate_code_challenge(code_verifier: str) -> str:
    """Generate code challenge dari code verifier (S256)"""
    digest = hashlib.sha256(code_verifier.encode()).digest()
    return base64.urlsafe_b64encode(digest).rstrip(b'=').decode()


@app.route('/login')
def login():
    """Mulai OAuth flow"""
    # Generate PKCE values
    code_verifier = generate_code_verifier()
    code_challenge = generate_code_challenge(code_verifier)
    state = secrets.token_urlsafe(32)

    # Simpan untuk verifikasi nanti
    session['oauth_state'] = state
    session['code_verifier'] = code_verifier

    # Buat authorization URL
    auth_params = {
        'client_id': CONFIG['client_id'],
        'redirect_uri': CONFIG['redirect_uri'],
        'response_type': 'code',
        'scope': ' '.join(CONFIG['scopes']),
        'state': state,
        'code_challenge': code_challenge,
        'code_challenge_method': 'S256',
    }

    auth_url = f"{CONFIG['auth_server_url']}/api/auth/oauth2/authorize?{urlencode(auth_params)}"

    # Redirect user ke SSO Tabalong
    return redirect(auth_url)


@app.route('/callback')
def callback():
    """Handle OAuth callback"""
    # Handle error dari OAuth server
    error = request.args.get('error')
    if error:
        return jsonify({
            'error': error,
            'error_description': request.args.get('error_description'),
        }), 400

    code = request.args.get('code')
    state = request.args.get('state')

    # Verifikasi state
    if state != session.get('oauth_state'):
        return jsonify({'error': 'Invalid state parameter'}), 400

    code_verifier = session.get('code_verifier')

    # Bersihkan session
    session.pop('oauth_state', None)
    session.pop('code_verifier', None)

    # Tukar code dengan token
    token_response = requests.post(
        f"{CONFIG['auth_server_url']}/api/auth/oauth2/token",
        data={
            'grant_type': 'authorization_code',
            'code': code,
            'redirect_uri': CONFIG['redirect_uri'],
            'client_id': CONFIG['client_id'],
            'client_secret': CONFIG['client_secret'],
            'code_verifier': code_verifier,
        },
        headers={
            'Content-Type': 'application/x-www-form-urlencoded',
        }
    )

    if not token_response.ok:
        return jsonify(token_response.json()), 400

    tokens = token_response.json()

    # Ambil user info
    userinfo_response = requests.get(
        f"{CONFIG['auth_server_url']}/api/auth/oauth2/userinfo",
        headers={
            'Authorization': f"Bearer {tokens['access_token']}",
        }
    )

    userinfo = userinfo_response.json()

    # Simpan di session (di produksi: buat session user, dll)
    session['user'] = userinfo
    session['tokens'] = tokens

    return jsonify({
        'tokens': tokens,
        'userinfo': userinfo,
    })


@app.route('/refresh', methods=['POST'])
def refresh():
    """Refresh access token"""
    refresh_token = request.json.get('refresh_token')

    response = requests.post(
        f"{CONFIG['auth_server_url']}/api/auth/oauth2/token",
        data={
            'grant_type': 'refresh_token',
            'refresh_token': refresh_token,
            'client_id': CONFIG['client_id'],
            'client_secret': CONFIG['client_secret'],
        },
        headers={
            'Content-Type': 'application/x-www-form-urlencoded',
        }
    )

    return jsonify(response.json())


@app.route('/userinfo')
def get_userinfo():
    """Ambil user info dari session"""
    user = session.get('user')
    if not user:
        return jsonify({'error': 'Not authenticated'}), 401
    return jsonify(user)


if __name__ == '__main__':
    app.run(port=5000, debug=True)