Created
April 1, 2015 03:37
-
-
Save yinhm/44dc12962dee8b0e2fa0 to your computer and use it in GitHub Desktop.
gin middleware for login via Google OAuth 2.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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