Skip to content

docs: oauth getting started #2129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/oauth2-oidc/_static/oauth2overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/oauth2-oidc/_static/ory-oauth2-outcome.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Callback handler
func handleCallback(w http.ResponseWriter, r *http.Request) {
// Get session cookie
cookie, err := r.Cookie("session_id")
if err != nil {
http.Error(w, "No session found", http.StatusBadRequest)
return
}

// Get session from store
session, ok := sessions[cookie.Value]
if !ok {
http.Error(w, "Invalid session", http.StatusBadRequest)
return
}

// Get authorization code and state from query parameters
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
if code == "" || state == "" || state != session.State {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}

// Exchange authorization code for token
token, err := oauthConfig.Exchange(
context.Background(),
code,
oauth2.VerifierOption(session.CodeVerifier),
)
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}

// Update session with token
session.Token = token
sessions[cookie.Value] = session

// Fetch user info
client := oauthConfig.Client(context.Background(), token)
userInfoURL := fmt.Sprintf("https://%s.projects.oryapis.com/userinfo", projectSlug)
resp, err := client.Get(userInfoURL)
if err != nil {
http.Error(w, "Failed to fetch user info: "+err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()

// Parse user info response
var userInfo map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
http.Error(w, "Failed to parse user info: "+err.Error(), http.StatusInternalServerError)
return
}

// Update session with user info
session.UserInfo = userInfo
sessions[cookie.Value] = session

// Redirect to home page
http.Redirect(w, r, "/", http.StatusSeeOther)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Login handler
func handleLogin(w http.ResponseWriter, r *http.Request) {
// Generate random state parameter
state, err := generateRandomString(32)
if err != nil {
http.Error(w, "Failed to generate state parameter", http.StatusInternalServerError)
return
}

// Generate code verifier for PKCE
codeVerifier := oauth2.GenerateVerifier()

// Create a new session
sessionID, err := generateRandomString(32)
if err != nil {
http.Error(w, "Failed to generate session ID", http.StatusInternalServerError)
return
}

// Store session
sessions[sessionID] = Session{
State: state,
CodeVerifier: codeVerifier,
}

// Set session cookie
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: sessionID,
Path: "/",
HttpOnly: true,
Secure: r.TLS != nil,
MaxAge: int(24 * time.Hour.Seconds()),
})

// Generate authorization URL with PKCE challenge
authURL := oauthConfig.AuthCodeURL(
state,
oauth2.S256ChallengeOption(codeVerifier),
)

// Redirect to authorization server
http.Redirect(w, r, authURL, http.StatusSeeOther)
}

// Generate a random string for state parameter
func generateRandomString(length int) (string, error) {
b := make([]byte, length)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
func handleLogout(w http.ResponseWriter, r *http.Request) {
// Get session cookie
cookie, err := r.Cookie("session_id")
if err == nil {
// Delete session
delete(sessions, cookie.Value)
}

// Clear cookie
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: "",
Path: "/",
HttpOnly: true,
Secure: r.TLS != nil,
MaxAge: -1,
})

// Redirect to home page
http.Redirect(w, r, "/", http.StatusSeeOther)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Profile handler (protected route)
func handleProfile(w http.ResponseWriter, r *http.Request) {
// Get session cookie
cookie, err := r.Cookie("session_id")
if err != nil {
http.Error(w, "No session found", http.StatusUnauthorized)
return
}

// Get session from store
session, ok := sessions[cookie.Value]
if !ok {
http.Error(w, "Invalid session", http.StatusUnauthorized)
return
}

// Return profile data
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "This is protected data from the resource server",
"user": session.UserInfo,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
func handleHome(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session_id")
var loggedIn bool
var userInfo map[string]interface{}

if err == nil {
if session, ok := sessions[cookie.Value]; ok && session.Token != nil && session.Token.Valid() {
loggedIn = true
userInfo = session.UserInfo
}
}

w.Header().Set("Content-Type", "text/html")
if loggedIn {
// Display logged-in page with user info
fmt.Fprintf(w, `
<html>
<head><title>OAuth2 Test</title></head>
<body>
<h1>Welcome!</h1>
<p>You are logged in.</p>
<h2>User Info:</h2>
<pre>%v</pre>
<p><a href="/profile">View Profile</a></p>
<p><a href="/logout">Logout</a></p>
</body>
</html>
`, userInfo)
} else {
// Display login page
fmt.Fprintf(w, `
<html>
<head><title>OAuth2 Test</title></head>
<body>
<h1>Welcome</h1>
<p>You are not logged in.</p>
<p><a href="/login">Login</a></p>
</body>
</html>
`)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// RequireAuth middleware with explicit token refresh
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Get session cookie
cookie, err := r.Cookie("session_id")
if err != nil {
// No session cookie, redirect to login
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}

// Get session from store
session, ok := sessions[cookie.Value]
if !ok {
// Session not found, redirect to login
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}

// Check if token is valid
if session.Token == nil || !session.Token.Valid() {
// Token expired, try to refresh it
if session.Token != nil && session.Token.RefreshToken != "" {
// Call the dedicated refresh function
newToken, err := RefreshToken(&session)
if err != nil {
// Refresh failed, redirect to login
log.Printf("Token refresh failed: %v", err)
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}

// Update session with new token
session.Token = newToken
sessions[cookie.Value] = session
} else {
// No refresh token, redirect to login
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
}

// Token is valid, proceed to the next handler
next(w, r)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// RefreshToken refreshes an expired token if a refresh token is available
func RefreshToken(session *Session) (*oauth2.Token, error) {
if session.Token == nil || session.Token.RefreshToken == "" {
return nil, fmt.Errorf("no refresh token available")
}

// Create a TokenSource with the current token
tokenSource := oauthConfig.TokenSource(context.Background(), session.Token)

// Get a new token (this will use the refresh token if the access token is expired)
newToken, err := tokenSource.Token()
if err != nil {
return nil, fmt.Errorf("failed to refresh token: %w", err)
}

// Log successful token refresh (optional)
log.Println("Token refreshed successfully")

return newToken, nil
}
44 changes: 44 additions & 0 deletions docs/oauth2-oidc/get-started/_common/code-examples/go/setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"fmt"
"os"

"golang.org/x/oauth2"
)

// Configuration
var (
// Replace these with your own values
clientID = os.Getenv("ORY_CLIENT_ID")
clientSecret = os.Getenv("ORY_CLIENT_SECRET")
projectSlug = os.Getenv("ORY_PROJECT_SLUG")
redirectURL = "http://localhost:8080/callback"
port = "8080"

// Ory OAuth2 endpoints
oryEndpoint = oauth2.Endpoint{
AuthURL: fmt.Sprintf("https://%s.projects.oryapis.com/oauth2/auth", projectSlug),
TokenURL: fmt.Sprintf("https://%s.projects.oryapis.com/oauth2/token", projectSlug),
}

// OAuth2 config
oauthConfig = &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURL,
Scopes: []string{"openid", "offline_access", "email"},
Endpoint: oryEndpoint,
}

// In-memory session store (replace with a proper session store in production)
sessions = make(map[string]Session)
)

// Session represents user session data
type Session struct {
State string
CodeVerifier string
Token *oauth2.Token
UserInfo map[string]interface{}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const requireAuth = (req, res, next) => {
if (!req.session.tokens || !req.session.tokens.access_token) {
// User is not authenticated, redirect to login
return res.redirect("/login")
}

// Check if the token is expired
if (req.session.tokens.expiresIn && req.session.tokens.expiresIn() <= 0) {
// Token is expired, we need to refresh it if we have a refresh token
if (req.session.tokens.refresh_token) {
// We'll handle refresh in a separate middleware
return refreshToken(req, res, next)
} else {
// No refresh token, redirect to login
return res.redirect("/login")
}
}

// User is authenticated with a valid token
next()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Middleware to refresh tokens
const refreshToken = async (req, res, next) => {
if (!config) {
return res
.status(500)
.send("Configuration not ready yet. Please try again in a moment.")
}

try {
// Use the refresh token to get new tokens
const tokens = await client.refreshTokenGrant(
config,
req.session.tokens.refresh_token,
)

// Update the tokens in the session
req.session.tokens = tokens

// Continue with the request
next()
} catch (error) {
console.error("Token refresh error:", error)
// Clear session and redirect to login
req.session.destroy()
res.redirect("/login")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
app.get("/callback", async (req, res) => {
if (!config) {
return res
.status(500)
.send("Configuration not ready yet. Please try again in a moment.")
}

try {
// Get the current URL
const currentUrl = new URL(req.url, `http://${req.headers.host}`)

// Exchange code for tokens with PKCE verification
const tokens = await client.authorizationCodeGrant(config, currentUrl, {
pkceCodeVerifier: req.session.codeVerifier,
expectedState: req.session.state,
})

// Store tokens in session
req.session.tokens = tokens

// Redirect to home page
res.redirect("/")
} catch (error) {
console.error("Callback error:", error)
res.status(500).send(`Authentication failed: ${error.message}`)
}
})
Empty file.
Loading
Loading