In the previous tutorial, you learned how to authenticate users — verifying who they are. But authentication alone is not enough. You also need authorization — controlling what they can do.

Authentication answers: “Who are you?” Authorization answers: “What are you allowed to do?”

Authentication vs Authorization

AuthenticationAuthorization
QuestionWho are you?What can you do?
WhenDuring loginAfter login, on every request
MethodPassword, JWT, biometricsRoles, permissions, policies
Example“You are user Alex”“Alex can read posts but not delete them”

A common mistake is checking authentication but skipping authorization. The user is logged in, so the app trusts them completely. This leads to Broken Access Control — the #1 risk in the OWASP Top 10.

Role-Based Access Control (RBAC)

RBAC is the most common authorization model. You assign roles to users, and each role has a set of permissions.

How RBAC Works

Users → Roles → Permissions

Alex  → admin  → [create, read, update, delete]
Sam   → editor → [create, read, update]
Guest → viewer → [read]

RBAC in Go

package main

import (
    "net/http"
)

type Role string

const (
    RoleAdmin  Role = "admin"
    RoleEditor Role = "editor"
    RoleViewer Role = "viewer"
)

// Permissions for each role
var rolePermissions = map[Role][]string{
    RoleAdmin:  {"create", "read", "update", "delete"},
    RoleEditor: {"create", "read", "update"},
    RoleViewer: {"read"},
}

func hasPermission(role Role, permission string) bool {
    permissions := rolePermissions[role]
    for _, p := range permissions {
        if p == permission {
            return true
        }
    }
    return false
}

// Middleware that checks permission
func requirePermission(permission string, next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        role := getUserRole(r) // Get from JWT or session
        if !hasPermission(role, permission) {
            http.Error(w, "Forbidden", http.StatusForbidden)
            return
        }
        next(w, r)
    }
}

RBAC in Python (Flask)

from functools import wraps
from flask import request, jsonify

ROLE_PERMISSIONS = {
    "admin":  ["create", "read", "update", "delete"],
    "editor": ["create", "read", "update"],
    "viewer": ["read"],
}

def require_permission(permission):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            role = get_user_role(request)  # Get from JWT or session
            if permission not in ROLE_PERMISSIONS.get(role, []):
                return jsonify({"error": "Forbidden"}), 403
            return f(*args, **kwargs)
        return wrapper
    return decorator

@app.route("/api/posts", methods=["POST"])
@require_permission("create")
def create_post():
    # Only admin and editor can reach here
    return jsonify({"message": "Post created"})

@app.route("/api/posts/<post_id>", methods=["DELETE"])
@require_permission("delete")
def delete_post(post_id):
    # Only admin can reach here
    return jsonify({"message": "Post deleted"})

RBAC in JavaScript (Express)

const ROLE_PERMISSIONS = {
    admin:  ['create', 'read', 'update', 'delete'],
    editor: ['create', 'read', 'update'],
    viewer: ['read'],
};

function requirePermission(permission) {
    return (req, res, next) => {
        const role = req.user.role; // From JWT middleware
        const permissions = ROLE_PERMISSIONS[role] || [];
        if (!permissions.includes(permission)) {
            return res.status(403).json({ error: 'Forbidden' });
        }
        next();
    };
}

// Usage
app.post('/api/posts', requirePermission('create'), createPost);
app.delete('/api/posts/:id', requirePermission('delete'), deletePost);

RBAC Best Practices

  1. Deny by default — if no permission is found, deny access
  2. Assign roles, not permissions — users get roles, roles have permissions
  3. Keep roles simple — 3-5 roles is usually enough
  4. Check on the server — never rely on client-side role checks
  5. Use middleware — centralize authorization logic, do not scatter it in handlers

OAuth 2.0: Delegated Authorization

OAuth 2.0 is an authorization framework. It lets users grant third-party applications limited access to their resources without sharing their password.

Example: When you click “Sign in with Google” on a website, you are using OAuth 2.0. You authorize the website to access your Google profile — without giving the website your Google password.

Key Concepts

TermMeaning
Resource OwnerThe user (you)
ClientThe application requesting access
Authorization ServerThe server that authenticates (Google, GitHub)
Resource ServerThe API that holds the data
Access TokenA short-lived token to access resources
Refresh TokenA long-lived token to get new access tokens
ScopeWhat the client can access (profile, email, repos)

OAuth 2.0 Grant Types

OAuth 2.0 defines several flows for different use cases:

1. Authorization Code (+ PKCE) — Web and Mobile Apps

This is the most secure and most common flow. It is the recommended flow for all new applications.

1. App redirects user to authorization server
2. User logs in and approves access
3. Authorization server redirects back with a code
4. App exchanges the code for tokens (server-to-server)
5. App uses the access token to call APIs

PKCE (Proof Key for Code Exchange) adds an extra layer of security for public clients (mobile apps, SPAs) that cannot keep a client secret.

1. App generates a random code_verifier
2. App creates code_challenge = SHA256(code_verifier)
3. App sends code_challenge with the authorization request
4. When exchanging the code, app sends code_verifier
5. Server verifies: SHA256(code_verifier) == code_challenge

2. Client Credentials — Server-to-Server

No user involved. One server authenticates to another using a client ID and secret.

1. Server sends client_id + client_secret to authorization server
2. Authorization server returns an access token
3. Server uses the token to call the API

3. Device Authorization — Smart TVs, CLI Tools

For devices that cannot open a browser or have limited input capabilities.

1. Device requests a device code and user code
2. Device shows: "Go to https://example.com/device and enter code: ABCD-1234"
3. User opens browser on phone, enters the code, and approves
4. Device polls the authorization server until approval is received
5. Device gets an access token

Authorization Code Flow in Go

package main

import (
    "net/http"
    "golang.org/x/oauth2"
    "golang.org/x/oauth2/google"
)

var oauthConfig = &oauth2.Config{
    ClientID:     "your-client-id",
    ClientSecret: "your-client-secret",
    RedirectURL:  "http://localhost:8080/callback",
    Scopes:       []string{"openid", "profile", "email"},
    Endpoint:     google.Endpoint,
}

func handleLogin(w http.ResponseWriter, r *http.Request) {
    // Generate a random state to prevent CSRF
    state := generateRandomState()
    storeState(state) // Save in session
    url := oauthConfig.AuthCodeURL(state)
    http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}

func handleCallback(w http.ResponseWriter, r *http.Request) {
    // Verify the state parameter
    state := r.URL.Query().Get("state")
    if !isValidState(state) {
        http.Error(w, "Invalid state", http.StatusBadRequest)
        return
    }

    // Exchange the code for a token
    code := r.URL.Query().Get("code")
    token, err := oauthConfig.Exchange(r.Context(), code)
    if err != nil {
        http.Error(w, "Token exchange failed", http.StatusInternalServerError)
        return
    }

    // Use token.AccessToken to call Google APIs
    _ = token
}

Common OAuth 2.0 Mistakes

MistakeImpactFix
Not validating the state parameterCSRF attacks — attacker can link their accountAlways generate and verify a random state
Using the Implicit flowTokens exposed in URL fragmentsUse Authorization Code + PKCE instead
Storing tokens in localStorageXSS can steal tokensUse httpOnly cookies or server-side sessions
Not validating redirect URIsAttacker can redirect tokens to their serverWhitelist exact redirect URIs
Requesting too many scopesUsers deny broad permissionsRequest minimum scopes needed

OpenID Connect (OIDC)

OAuth 2.0 is for authorization (accessing resources). It does not tell you WHO the user is. OpenID Connect (OIDC) adds an authentication layer on top of OAuth 2.0.

What OIDC Adds

FeatureOAuth 2.0OIDC
PurposeAuthorizationAuthentication + Authorization
User identityNoYes (ID Token)
Token typeAccess tokenAccess token + ID token
User infoNot standardizedStandardized /userinfo endpoint

ID Token

The ID token is a JWT that contains user information:

{
  "iss": "https://accounts.google.com",
  "sub": "1234567890",
  "email": "alex@example.com",
  "name": "Alex",
  "iat": 1700000000,
  "exp": 1700003600
}

Key claims:

  • iss — who issued the token (Google, GitHub, etc.)
  • sub — unique user ID at the provider
  • email — user’s email (if the email scope was requested)
  • exp — when the token expires

Implementing “Login with Google”

Here is a simplified flow:

// 1. Redirect user to Google
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
    `client_id=${CLIENT_ID}` +
    `&redirect_uri=${REDIRECT_URI}` +
    `&response_type=code` +
    `&scope=openid profile email` +
    `&state=${randomState}`;

// 2. Handle the callback
app.get('/callback', async (req, res) => {
    const { code, state } = req.query;

    // Verify state
    if (state !== expectedState) {
        return res.status(400).send('Invalid state');
    }

    // 3. Exchange code for tokens
    const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
        method: 'POST',
        body: new URLSearchParams({
            code,
            client_id: CLIENT_ID,
            client_secret: CLIENT_SECRET,
            redirect_uri: REDIRECT_URI,
            grant_type: 'authorization_code',
        }),
    });

    const { access_token, id_token } = await tokenResponse.json();

    // 4. Decode and verify the ID token to get user info
    const user = jwt.verify(id_token, googlePublicKey, {
        algorithms: ['RS256'],
        issuer: 'https://accounts.google.com',
        audience: CLIENT_ID, // Verify the token was meant for your app
    });

    console.log('User:', user.name, user.email);
});

RBAC vs OAuth 2.0 vs OIDC

ModelPurposeWhen to Use
RBACInternal authorizationControl what your users can do in YOUR app
OAuth 2.0Delegated authorizationLet third-party apps access your users’ data
OIDCFederated authentication“Login with Google/GitHub/Microsoft”

Most applications use all three:

  • OIDC to let users sign in with Google
  • OAuth 2.0 to manage API access tokens
  • RBAC to control what each user can do inside the app

Prevention Checklist

TopicBest Practice
Authorization checksAlways check on the server, every request
Default policyDeny by default — whitelist allowed actions
RBACUse roles and permissions, not hardcoded checks
OAuth state parameterGenerate and validate random state (CSRF protection)
OAuth flowUse Authorization Code + PKCE for all clients
Redirect URIsWhitelist exact URIs — no wildcards
ScopesRequest minimum scopes needed
OIDC ID tokensAlways verify signature, issuer, and expiration
Token storagehttpOnly cookies or server-side sessions
Implicit flowDo not use — deprecated and insecure

What’s Next?

In the next tutorial, we will cover the two most common injection attacks — SQL injection and XSS (Cross-Site Scripting). You will learn how attackers exploit them and, more importantly, how to prevent them in your code.