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
| Authentication | Authorization | |
|---|---|---|
| Question | Who are you? | What can you do? |
| When | During login | After login, on every request |
| Method | Password, JWT, biometrics | Roles, 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
- Deny by default — if no permission is found, deny access
- Assign roles, not permissions — users get roles, roles have permissions
- Keep roles simple — 3-5 roles is usually enough
- Check on the server — never rely on client-side role checks
- 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
| Term | Meaning |
|---|---|
| Resource Owner | The user (you) |
| Client | The application requesting access |
| Authorization Server | The server that authenticates (Google, GitHub) |
| Resource Server | The API that holds the data |
| Access Token | A short-lived token to access resources |
| Refresh Token | A long-lived token to get new access tokens |
| Scope | What 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
| Mistake | Impact | Fix |
|---|---|---|
Not validating the state parameter | CSRF attacks — attacker can link their account | Always generate and verify a random state |
| Using the Implicit flow | Tokens exposed in URL fragments | Use Authorization Code + PKCE instead |
| Storing tokens in localStorage | XSS can steal tokens | Use httpOnly cookies or server-side sessions |
| Not validating redirect URIs | Attacker can redirect tokens to their server | Whitelist exact redirect URIs |
| Requesting too many scopes | Users deny broad permissions | Request 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
| Feature | OAuth 2.0 | OIDC |
|---|---|---|
| Purpose | Authorization | Authentication + Authorization |
| User identity | No | Yes (ID Token) |
| Token type | Access token | Access token + ID token |
| User info | Not standardized | Standardized /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 provideremail— user’s email (if theemailscope 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
| Model | Purpose | When to Use |
|---|---|---|
| RBAC | Internal authorization | Control what your users can do in YOUR app |
| OAuth 2.0 | Delegated authorization | Let third-party apps access your users’ data |
| OIDC | Federated 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
| Topic | Best Practice |
|---|---|
| Authorization checks | Always check on the server, every request |
| Default policy | Deny by default — whitelist allowed actions |
| RBAC | Use roles and permissions, not hardcoded checks |
| OAuth state parameter | Generate and validate random state (CSRF protection) |
| OAuth flow | Use Authorization Code + PKCE for all clients |
| Redirect URIs | Whitelist exact URIs — no wildcards |
| Scopes | Request minimum scopes needed |
| OIDC ID tokens | Always verify signature, issuer, and expiration |
| Token storage | httpOnly cookies or server-side sessions |
| Implicit flow | Do not use — deprecated and insecure |
Related Articles
- Security Tutorial #1: Web Security Basics — OWASP Top 10
- Security Tutorial #2: Authentication — Passwords, Hashing, JWT
- Security Tutorial #4: SQL Injection and XSS — How to Prevent Them
- Security Tutorial #5: HTTPS and TLS — How Encryption Works
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.