Skip to content

Instantly share code, notes, and snippets.

@yinhm
Created April 1, 2015 03:37
Show Gist options
  • Save yinhm/44dc12962dee8b0e2fa0 to your computer and use it in GitHub Desktop.
Save yinhm/44dc12962dee8b0e2fa0 to your computer and use it in GitHub Desktop.
gin middleware for login via Google OAuth 2.0
// Copyright 2014 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package oauth2 contains gin handlers to provide
// user login via an OAuth 2.0 backend.
//
// Usage:
// r.Use(server.GoogleAuthFromConfig(options.KeyFile, options.Debug))
//
package auth
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"time"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
const (
codeRedirect = 302
keyToken = "oauth2_token"
keyNextPage = "next"
)
var (
// PathLogin is the path to handle OAuth 2.0 logins.
PathLogin = "/login"
// PathLogout is the path to handle OAuth 2.0 logouts.
PathLogout = "/logout"
// PathCallback is the path to handle callback from OAuth 2.0 backend
// to exchange credentials.
PathCallback = "/auth/google/callback"
// PathError is the path to handle error cases.
PathError = "/unauthorized"
)
type UserInfo struct {
Id string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Picture string `json:"picture"`
Locale string `json:"locale"`
}
// Tokens represents a container that contains user's OAuth 2.0 access and refresh tokens.
type Tokens interface {
Access() string
Refresh() string
Expired() bool
ExpiryTime() time.Time
}
type token struct {
oauth2.Token
}
// Access returns the access token.
func (t *token) Access() string {
return t.AccessToken
}
// Refresh returns the refresh token.
func (t *token) Refresh() string {
return t.RefreshToken
}
// Expired returns whether the access token is expired or not.
func (t *token) Expired() bool {
if t == nil {
return true
}
return !t.Token.Valid()
}
// ExpiryTime returns the expiry time of the user's access token.
func (t *token) ExpiryTime() time.Time {
return t.Expiry
}
// String returns the string representation of the token.
func (t *token) String() string {
return fmt.Sprintf("tokens: %s expire at: %s", t.Access(), t.ExpiryTime())
}
// Google returns a new Google OAuth 2.0 backend endpoint.
func Google(conf *oauth2.Config) gin.HandlerFunc {
return NewOAuth2Provider(conf)
}
// NewOAuth2Provider returns a generic OAuth 2.0 backend endpoint.
func NewOAuth2Provider(conf *oauth2.Config) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method == "GET" {
switch c.Request.URL.Path {
case PathLogin:
login(conf, c)
case PathLogout:
logout(c)
case PathCallback:
handleOAuth2Callback(conf, c)
}
}
s := sessions.Default(c)
tk := unmarshallToken(s)
if tk != nil {
// check if the access token is expired
if tk.Expired() && tk.Refresh() == "" {
s.Delete(keyToken)
s.Save()
tk = nil
}
}
}
}
// Handler that redirects user to the login page
// if user is not logged in.
// Sample usage:
// m.Get("/login-required", oauth2.LoginRequired, func() ... {})
var LoginRequired = func() gin.HandlerFunc {
return func(c *gin.Context) {
s := sessions.Default(c)
token := unmarshallToken(s)
if token == nil || token.Expired() {
next := url.QueryEscape(c.Request.URL.RequestURI())
http.Redirect(c.Writer, c.Request, PathLogin+"?next="+next, codeRedirect)
}
}
}()
func login(f *oauth2.Config, c *gin.Context) {
s := sessions.Default(c)
next := extractPath(c.Request.URL.Query().Get(keyNextPage))
if s.Get(keyToken) == nil {
// User is not logged in.
if next == "" {
next = "/"
}
http.Redirect(c.Writer, c.Request, f.AuthCodeURL(next), codeRedirect)
return
}
// No need to login, redirect to the next page.
http.Redirect(c.Writer, c.Request, next, codeRedirect)
}
func logout(c *gin.Context) {
s := sessions.Default(c)
next := extractPath(c.Request.URL.Query().Get(keyNextPage))
s.Delete(keyToken)
s.Save()
http.Redirect(c.Writer, c.Request, next, codeRedirect)
}
func handleOAuth2Callback(f *oauth2.Config, c *gin.Context) {
s := sessions.Default(c)
next := extractPath(c.Request.URL.Query().Get("state"))
code := c.Request.URL.Query().Get("code")
t, err := f.Exchange(oauth2.NoContext, code)
if err != nil {
// Pass the error message, or allow dev to provide its own
// error handler.
log.Println("exchange oauth token failed:", err)
http.Redirect(c.Writer, c.Request, PathError, codeRedirect)
return
}
// Store the credentials in the session.
val, _ := json.Marshal(t)
s.Set(keyToken, val)
s.Save()
http.Redirect(c.Writer, c.Request, next, codeRedirect)
}
func unmarshallToken(s sessions.Session) (t *token) {
if s.Get(keyToken) == nil {
return
}
data := s.Get(keyToken).([]byte)
var tk oauth2.Token
json.Unmarshal(data, &tk)
return &token{tk}
}
func extractPath(next string) string {
n, err := url.Parse(next)
if err != nil {
return "/"
}
return n.Path
}
func GoogleAuthConfig(keyPath string, debug bool) *oauth2.Config {
jsonKey, err := ioutil.ReadFile(keyPath)
if err != nil {
log.Fatal(err)
}
conf, _ := google.ConfigFromJSON(jsonKey, "profile")
if debug {
conf.RedirectURL = "http://localhost:8080/auth/google/callback"
}
return conf
}
func GoogleAuthFromConfig(keyPath string, debug bool) gin.HandlerFunc {
return Google(GoogleAuthConfig(keyPath, debug))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment