Skip to main content

Echo OAuth Integration

Introduction

OAuth (Open Authorization) is an open standard authentication protocol that allows secure authorization from web, mobile, and desktop applications. When building web applications with Echo, implementing OAuth enables your users to log in using their existing accounts from providers like Google, Facebook, GitHub, or any custom OAuth provider.

In this guide, we'll explore how to integrate OAuth authentication into your Echo application. We'll cover the basic OAuth concepts, implementation steps, and provide practical examples to secure your Echo routes with OAuth.

Understanding OAuth

OAuth is primarily about authorization—allowing a third-party application to access a user's data without exposing their credentials. OAuth works through a series of token exchanges:

  1. Client Registration: Your application registers with an OAuth provider to get client credentials
  2. Authorization Request: User is redirected to the OAuth provider to grant permissions
  3. Authorization Grant: Provider gives your application an authorization code
  4. Access Token Request: Your application exchanges this code for an access token
  5. Protected Resource Access: The access token is used to request protected resources

Prerequisites

Before we start, make sure you have:

  • Go installed on your system
  • Basic knowledge of Echo framework
  • An OAuth provider account (Google, GitHub, etc.)

Setting up Echo OAuth Integration

Step 1: Install Required Packages

We'll use the popular go-oauth2/oauth2 package alongside Echo:

bash
go get -u github.com/labstack/echo/v4
go get -u github.com/go-oauth2/oauth2/v4
go get -u github.com/go-oauth2/oauth2/v4/server

Step 2: Create the OAuth Server Configuration

Let's create a basic Echo application with OAuth server configuration:

go
package main

import (
"net/http"
"time"

"github.com/go-oauth2/oauth2/v4/manage"
"github.com/go-oauth2/oauth2/v4/models"
"github.com/go-oauth2/oauth2/v4/server"
"github.com/go-oauth2/oauth2/v4/store"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

func main() {
// Echo instance
e := echo.New()

// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// OAuth2 manager
manager := manage.NewDefaultManager()

// Set token store
manager.MustTokenStorage(store.NewMemoryTokenStore())

// Set client store
clientStore := store.NewClientStore()
clientStore.Set("client1", &models.Client{
ID: "client1",
Secret: "secret1",
Domain: "http://localhost:8080",
})
manager.MapClientStorage(clientStore)

// OAuth2 server
srv := server.NewServer(server.NewConfig(), manager)

// Start server
e.Logger.Fatal(e.Start(":1323"))
}

Step 3: Implement OAuth Endpoints

Now let's add the necessary OAuth endpoints to our Echo application:

go
func main() {
// Previous code...

// OAuth authorization endpoint
e.GET("/authorize", func(c echo.Context) error {
err := srv.HandleAuthorizeRequest(c.Response(), c.Request())
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
return nil
})

// OAuth token endpoint
e.POST("/token", func(c echo.Context) error {
err := srv.HandleTokenRequest(c.Response(), c.Request())
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
return nil
})

// Protected resource
e.GET("/protected", func(c echo.Context) error {
tokenInfo, err := srv.ValidationBearerToken(c.Request())
if err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "You accessed a protected resource!",
"client": tokenInfo.GetClientID(),
"expires": tokenInfo.GetAccessCreateAt().Add(tokenInfo.GetAccessExpiresIn()),
})
})

// Start server
e.Logger.Fatal(e.Start(":1323"))
}

Implementing OAuth Client Flow

Let's see how to implement the OAuth client flow, where your Echo application acts as an OAuth client:

Step 1: Create a Client Configuration

go
package main

import (
"context"
"fmt"
"net/http"

"github.com/labstack/echo/v4"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github" // Example using GitHub as provider
)

// OAuth configuration
var oauthConfig = &oauth2.Config{
ClientID: "YOUR_CLIENT_ID", // Replace with your OAuth client ID
ClientSecret: "YOUR_CLIENT_SECRET", // Replace with your OAuth client secret
RedirectURL: "http://localhost:1323/callback",
Scopes: []string{"user:email"},
Endpoint: github.Endpoint,
}

func main() {
e := echo.New()

// Home page with login link
e.GET("/", func(c echo.Context) error {
return c.HTML(http.StatusOK, `
<html>
<body>
<h1>Echo OAuth Example</h1>
<a href="/login">Login with GitHub</a>
</body>
</html>
`)
})

// Login route - redirects to OAuth provider
e.GET("/login", func(c echo.Context) error {
// State parameter helps prevent CSRF attacks
url := oauthConfig.AuthCodeURL("state-token", oauth2.AccessTypeOnline)
return c.Redirect(http.StatusTemporaryRedirect, url)
})

// OAuth callback handler
e.GET("/callback", func(c echo.Context) error {
// Get authorization code from query parameters
code := c.QueryParam("code")
state := c.QueryParam("state")

if state != "state-token" {
return c.String(http.StatusBadRequest, "State mismatch")
}

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

// Use the token to get user info or store it for future API calls
return c.JSON(http.StatusOK, map[string]interface{}{
"access_token": token.AccessToken,
"token_type": token.TokenType,
"expires_in": token.Expiry,
})
})

e.Start(":1323")
}

Step 2: Using the OAuth Token to Access Protected Resources

Let's extend our example to fetch user information using the OAuth token:

go
// Add this after the callback handler
e.GET("/profile", func(c echo.Context) error {
// Get token from session/cookie (implementation depends on your setup)
tokenStr := c.Request().Header.Get("Authorization")
if tokenStr == "" {
return c.String(http.StatusUnauthorized, "No token provided")
}

// Remove "Bearer " prefix if present
if len(tokenStr) > 7 && tokenStr[:7] == "Bearer " {
tokenStr = tokenStr[7:]
}

// Create a token object
token := &oauth2.Token{
AccessToken: tokenStr,
TokenType: "Bearer",
}

// Create HTTP client using the token
client := oauthConfig.Client(context.Background(), token)

// Make API request to get user profile (GitHub API in this example)
resp, err := client.Get("https://api.github.com/user")
if err != nil {
return c.String(http.StatusInternalServerError, "Failed to get user info: "+err.Error())
}
defer resp.Body.Close()

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

return c.JSON(http.StatusOK, userInfo)
})

Creating an OAuth Middleware for Echo

To make it easier to protect routes, let's create a reusable OAuth middleware:

go
// OAuthMiddleware verifies the OAuth token in the request
func OAuthMiddleware(oauthServer *server.Server) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Validate the token
tokenInfo, err := oauthServer.ValidationBearerToken(c.Request())
if err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid or expired token",
})
}

// Add token info to context
c.Set("oauth_token", tokenInfo)

// Call the next handler
return next(c)
}
}
}

Usage example:

go
// Create a group for protected routes
api := e.Group("/api")

// Apply the OAuth middleware to the group
api.Use(OAuthMiddleware(srv))

// Protected routes
api.GET("/users", func(c echo.Context) error {
// Access token info from context if needed
tokenInfo := c.Get("oauth_token")

return c.JSON(http.StatusOK, map[string]interface{}{
"users": []string{"user1", "user2", "user3"},
"token_client": tokenInfo.GetClientID(),
})
})

Real-World Use Case: Single Sign-On with Multiple Providers

Here's a more complete example showing how to implement Single Sign-On (SSO) with multiple OAuth providers:

go
package main

import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
"golang.org/x/oauth2/google"
)

// OAuthProvider defines a configured OAuth provider
type OAuthProvider struct {
Name string
Config *oauth2.Config
UserInfoURL string
}

// User represents the authenticated user
type User struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Provider string `json:"provider"`
CreatedAt time.Time `json:"created_at"`
}

// Configure our OAuth providers
var providers = map[string]OAuthProvider{
"github": {
Name: "GitHub",
Config: &oauth2.Config{
ClientID: "YOUR_GITHUB_CLIENT_ID",
ClientSecret: "YOUR_GITHUB_CLIENT_SECRET",
RedirectURL: "http://localhost:1323/auth/callback/github",
Scopes: []string{"user:email"},
Endpoint: github.Endpoint,
},
UserInfoURL: "https://api.github.com/user",
},
"google": {
Name: "Google",
Config: &oauth2.Config{
ClientID: "YOUR_GOOGLE_CLIENT_ID",
ClientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
RedirectURL: "http://localhost:1323/auth/callback/google",
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
Endpoint: google.Endpoint,
},
UserInfoURL: "https://www.googleapis.com/oauth2/v3/userinfo",
},
}

func main() {
e := echo.New()

// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())

// Session middleware would be added here in a real application
// e.Use(session.Middleware(sessions.NewCookieStore([]byte("secret"))))

// Home page
e.GET("/", func(c echo.Context) error {
html := `<html><body><h1>Echo OAuth SSO Example</h1><ul>`
for id, provider := range providers {
html += fmt.Sprintf(`<li><a href="/auth/login/%s">Login with %s</a></li>`, id, provider.Name)
}
html += `</ul></body></html>`

return c.HTML(http.StatusOK, html)
})

// Auth routes
auth := e.Group("/auth")

// Login routes for each provider
auth.GET("/login/:provider", func(c echo.Context) error {
providerName := c.Param("provider")
provider, exists := providers[providerName]

if !exists {
return c.String(http.StatusBadRequest, "Unknown provider")
}

// Generate random state for CSRF protection
state := fmt.Sprintf("%d", time.Now().UnixNano())

// In a real app, you'd store the state in session
// session := sessions.Default(c)
// session.Set("oauth-state", state)
// session.Save()

// Redirect to provider's authorization page
url := provider.Config.AuthCodeURL(state)
return c.Redirect(http.StatusTemporaryRedirect, url)
})

// Callback handler for each provider
auth.GET("/callback/:provider", func(c echo.Context) error {
providerName := c.Param("provider")
provider, exists := providers[providerName]

if !exists {
return c.String(http.StatusBadRequest, "Unknown provider")
}

// Get authorization code from query parameters
code := c.QueryParam("code")
state := c.QueryParam("state")

// In a real app, you'd verify the state matches what's in session
// session := sessions.Default(c)
// expectedState := session.Get("oauth-state")
// if state != expectedState {
// return c.String(http.StatusBadRequest, "Invalid state parameter")
// }

// Exchange authorization code for token
token, err := provider.Config.Exchange(context.Background(), code)
if err != nil {
return c.String(http.StatusInternalServerError,
"Failed to exchange token: "+err.Error())
}

// Use the token to get user info
client := provider.Config.Client(context.Background(), token)
resp, err := client.Get(provider.UserInfoURL)
if err != nil {
return c.String(http.StatusInternalServerError,
"Failed to get user info: "+err.Error())
}
defer resp.Body.Close()

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

// Create user from provider data (mapping depends on provider)
user := User{
Provider: providerName,
CreatedAt: time.Now(),
}

// Map provider-specific fields (simplified example)
if providerName == "github" {
user.ID = fmt.Sprintf("%v", userInfo["id"])
user.Name = fmt.Sprintf("%v", userInfo["name"])
user.Email = fmt.Sprintf("%v", userInfo["email"])
} else if providerName == "google" {
user.ID = fmt.Sprintf("%v", userInfo["sub"])
user.Name = fmt.Sprintf("%v", userInfo["name"])
user.Email = fmt.Sprintf("%v", userInfo["email"])
}

// In a real app, you'd store user in database and start a session
// session.Set("user", user)
// session.Save()

// Return user info for demo purposes
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Authentication successful",
"user": user,
"token": token.AccessToken,
})
})

// Protected API endpoint example
api := e.Group("/api")
api.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// In a real app, you'd check session or JWT here
auth := c.Request().Header.Get("Authorization")
if auth == "" || auth != "Bearer sample-token" {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Unauthorized",
})
}
return next(c)
}
})

api.GET("/profile", func(c echo.Context) error {
// In a real app, you'd get the user from session or token
user := User{
ID: "123",
Name: "John Doe",
Email: "[email protected]",
Provider: "github",
}

return c.JSON(http.StatusOK, user)
})

e.Logger.Fatal(e.Start(":1323"))
}

Best Practices for OAuth Implementation

  1. Always Use HTTPS: OAuth 2.0 relies on TLS/SSL for security.
  2. Implement State Parameter: Use the state parameter to prevent CSRF attacks.
  3. Validate Tokens: Always validate tokens on the server-side before granting access.
  4. Store Tokens Securely: Never store access tokens in localStorage or cookies without proper security measures.
  5. Use Short-lived Tokens: Configure your OAuth server to issue short-lived access tokens.
  6. Implement Refresh Tokens: Use refresh tokens to obtain new access tokens without user intervention.
  7. Validate Scopes: Make sure tokens only have the scopes they need for the requested operation.

Summary

In this guide, we've covered how to integrate OAuth authentication into your Echo applications. We've implemented both the OAuth server and client sides, showing how to:

  1. Set up an OAuth 2.0 server with Echo
  2. Implement OAuth client authentication flow
  3. Create a middleware to protect routes
  4. Implement SSO with multiple OAuth providers

OAuth integration allows your Echo applications to securely authenticate users through trusted third-party providers, providing a better user experience while maintaining strong security.

Additional Resources

Exercises

  1. Implement a complete OAuth solution using Echo and a database to persist tokens
  2. Add Facebook as an additional OAuth provider to the SSO example
  3. Create a JWT-based authentication system that works alongside OAuth
  4. Build a simple API gateway that validates OAuth tokens before proxying requests
  5. Implement token revocation and introspection endpoints in your OAuth server


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)