2015-03-13 04:20:36 +02:00
|
|
|
package oauth2
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/rand"
|
|
|
|
"encoding/base64"
|
2015-03-25 04:39:20 +02:00
|
|
|
"encoding/json"
|
2015-03-13 04:20:36 +02:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2015-03-14 01:23:43 +02:00
|
|
|
"net/url"
|
2015-03-13 04:20:36 +02:00
|
|
|
"path"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"golang.org/x/oauth2"
|
|
|
|
"gopkg.in/authboss.v0"
|
2015-03-28 18:08:05 +02:00
|
|
|
"gopkg.in/authboss.v0/internal/response"
|
2015-03-13 04:20:36 +02:00
|
|
|
)
|
|
|
|
|
2015-03-13 11:15:58 +02:00
|
|
|
var (
|
|
|
|
errOAuthStateValidation = errors.New("Could not validate oauth2 state param")
|
|
|
|
)
|
|
|
|
|
2015-03-16 23:42:45 +02:00
|
|
|
// OAuth2 module
|
2015-03-13 04:20:36 +02:00
|
|
|
type OAuth2 struct{}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
authboss.RegisterModule("oauth2", &OAuth2{})
|
|
|
|
}
|
|
|
|
|
2015-03-16 23:42:45 +02:00
|
|
|
// Initialize module
|
2015-03-13 04:20:36 +02:00
|
|
|
func (o *OAuth2) Initialize() error {
|
2015-03-31 21:34:03 +02:00
|
|
|
if authboss.a.OAuth2Storer == nil {
|
2015-03-13 04:20:36 +02:00
|
|
|
return errors.New("oauth2: need an OAuth2Storer")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-03-16 23:42:45 +02:00
|
|
|
// Routes for module
|
2015-03-13 04:20:36 +02:00
|
|
|
func (o *OAuth2) Routes() authboss.RouteTable {
|
|
|
|
routes := make(authboss.RouteTable)
|
|
|
|
|
2015-03-31 21:34:03 +02:00
|
|
|
for prov, cfg := range authboss.a.OAuth2Providers {
|
2015-03-13 04:20:36 +02:00
|
|
|
prov = strings.ToLower(prov)
|
|
|
|
|
|
|
|
init := fmt.Sprintf("/oauth2/%s", prov)
|
|
|
|
callback := fmt.Sprintf("/oauth2/callback/%s", prov)
|
|
|
|
|
2015-03-14 07:09:01 +02:00
|
|
|
routes[init] = oauthInit
|
|
|
|
routes[callback] = oauthCallback
|
|
|
|
|
2015-03-31 21:34:03 +02:00
|
|
|
if len(authboss.a.MountPath) > 0 {
|
|
|
|
callback = path.Join(authboss.a.MountPath, callback)
|
2015-03-13 04:20:36 +02:00
|
|
|
}
|
2015-03-13 11:15:58 +02:00
|
|
|
|
2015-03-31 21:34:03 +02:00
|
|
|
a.OAuth2Config.RedirectURL = authboss.a.RootURL + callback
|
2015-03-13 04:20:36 +02:00
|
|
|
}
|
|
|
|
|
2015-03-17 06:46:15 +02:00
|
|
|
routes["/oauth2/logout"] = logout
|
|
|
|
|
2015-03-13 04:20:36 +02:00
|
|
|
return routes
|
|
|
|
}
|
|
|
|
|
2015-03-16 23:42:45 +02:00
|
|
|
// Storage requirements
|
2015-03-13 04:20:36 +02:00
|
|
|
func (o *OAuth2) Storage() authboss.StorageOptions {
|
|
|
|
return authboss.StorageOptions{
|
2015-03-14 01:23:43 +02:00
|
|
|
authboss.StoreEmail: authboss.String,
|
|
|
|
authboss.StoreOAuth2UID: authboss.String,
|
|
|
|
authboss.StoreOAuth2Provider: authboss.String,
|
|
|
|
authboss.StoreOAuth2Token: authboss.String,
|
|
|
|
authboss.StoreOAuth2Refresh: authboss.String,
|
|
|
|
authboss.StoreOAuth2Expiry: authboss.DateTime,
|
2015-03-13 04:20:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func oauthInit(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) error {
|
|
|
|
provider := strings.ToLower(filepath.Base(r.URL.Path))
|
2015-03-31 21:34:03 +02:00
|
|
|
cfg, ok := authboss.a.OAuth2Providers[provider]
|
2015-03-13 04:20:36 +02:00
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("OAuth2 provider %q not found", provider)
|
|
|
|
}
|
|
|
|
|
|
|
|
random := make([]byte, 32)
|
|
|
|
_, err := rand.Read(random)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
state := base64.URLEncoding.EncodeToString(random)
|
|
|
|
ctx.SessionStorer.Put(authboss.SessionOAuth2State, state)
|
|
|
|
|
2015-03-25 04:39:20 +02:00
|
|
|
passAlongs := make(map[string]string)
|
2015-03-14 01:23:43 +02:00
|
|
|
for k, vals := range r.URL.Query() {
|
|
|
|
for _, val := range vals {
|
2015-03-25 04:39:20 +02:00
|
|
|
passAlongs[k] = val
|
2015-03-14 01:23:43 +02:00
|
|
|
}
|
|
|
|
}
|
2015-03-25 04:39:20 +02:00
|
|
|
|
2015-03-14 01:23:43 +02:00
|
|
|
if len(passAlongs) > 0 {
|
2015-03-25 04:39:20 +02:00
|
|
|
str, err := json.Marshal(passAlongs)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
ctx.SessionStorer.Put(authboss.SessionOAuth2Params, string(str))
|
|
|
|
} else {
|
|
|
|
ctx.SessionStorer.Del(authboss.SessionOAuth2Params)
|
2015-03-14 01:23:43 +02:00
|
|
|
}
|
|
|
|
|
2015-03-31 21:34:03 +02:00
|
|
|
url := a.OAuth2Config.AuthCodeURL(state)
|
2015-03-13 04:20:36 +02:00
|
|
|
|
2015-03-31 21:34:03 +02:00
|
|
|
extraParams := a.AdditionalParams.Encode()
|
2015-03-13 04:20:36 +02:00
|
|
|
if len(extraParams) > 0 {
|
|
|
|
url = fmt.Sprintf("%s&%s", url, extraParams)
|
|
|
|
}
|
|
|
|
|
|
|
|
http.Redirect(w, r, url, http.StatusFound)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-03-13 11:15:58 +02:00
|
|
|
// for testing
|
|
|
|
var exchanger = (*oauth2.Config).Exchange
|
|
|
|
|
2015-03-13 04:20:36 +02:00
|
|
|
func oauthCallback(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) error {
|
|
|
|
provider := strings.ToLower(filepath.Base(r.URL.Path))
|
|
|
|
|
2015-03-25 04:39:20 +02:00
|
|
|
sessState, err := ctx.SessionStorer.GetErr(authboss.SessionOAuth2State)
|
|
|
|
ctx.SessionStorer.Del(authboss.SessionOAuth2State)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
sessValues, ok := ctx.SessionStorer.Get(authboss.SessionOAuth2Params)
|
|
|
|
// Don't delete this value from session immediately, callbacks use this too
|
|
|
|
var values map[string]string
|
|
|
|
if ok {
|
|
|
|
if err := json.Unmarshal([]byte(sessValues), &values); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-13 04:20:36 +02:00
|
|
|
hasErr := r.FormValue("error")
|
|
|
|
if len(hasErr) > 0 {
|
2015-03-31 21:34:03 +02:00
|
|
|
if err := authboss.a.Callbacks.FireAfter(authboss.EventOAuthFail, ctx); err != nil {
|
2015-03-14 01:23:43 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-03-13 04:20:36 +02:00
|
|
|
return authboss.ErrAndRedirect{
|
|
|
|
Err: errors.New(r.FormValue("error_reason")),
|
2015-03-31 21:34:03 +02:00
|
|
|
Location: authboss.a.AuthLoginFailPath,
|
2015-03-13 04:20:36 +02:00
|
|
|
FlashError: fmt.Sprintf("%s login cancelled or failed.", strings.Title(provider)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-31 21:34:03 +02:00
|
|
|
cfg, ok := authboss.a.OAuth2Providers[provider]
|
2015-03-13 04:20:36 +02:00
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("OAuth2 provider %q not found", provider)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure request is genuine
|
2015-03-14 01:23:43 +02:00
|
|
|
state := r.FormValue(authboss.FormValueOAuth2State)
|
|
|
|
splState := strings.Split(state, ";")
|
|
|
|
if len(splState) == 0 || splState[0] != sessState {
|
2015-03-13 11:15:58 +02:00
|
|
|
return errOAuthStateValidation
|
2015-03-13 04:20:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get the code
|
|
|
|
code := r.FormValue("code")
|
2015-03-31 21:34:03 +02:00
|
|
|
token, err := exchanger(a.OAuth2Config, oauth2.NoContext, code)
|
2015-03-13 04:20:36 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Could not validate oauth2 code: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-03-31 21:34:03 +02:00
|
|
|
user, err := a.Callback(*cfg.OAuth2Config, token)
|
2015-03-14 01:23:43 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// OAuth2UID is required.
|
|
|
|
uid, err := user.StringErr(authboss.StoreOAuth2UID)
|
2015-03-13 04:20:36 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-03-14 01:23:43 +02:00
|
|
|
user[authboss.StoreOAuth2UID] = uid
|
|
|
|
user[authboss.StoreOAuth2Provider] = provider
|
2015-03-13 11:15:58 +02:00
|
|
|
user[authboss.StoreOAuth2Expiry] = token.Expiry
|
|
|
|
user[authboss.StoreOAuth2Token] = token.AccessToken
|
|
|
|
if len(token.RefreshToken) != 0 {
|
|
|
|
user[authboss.StoreOAuth2Refresh] = token.RefreshToken
|
|
|
|
}
|
2015-03-14 01:23:43 +02:00
|
|
|
|
2015-03-31 21:34:03 +02:00
|
|
|
if err = authboss.a.OAuth2Storer.PutOAuth(uid, provider, user); err != nil {
|
2015-03-14 01:23:43 +02:00
|
|
|
return err
|
2015-03-13 04:20:36 +02:00
|
|
|
}
|
|
|
|
|
2015-03-15 17:06:08 +02:00
|
|
|
// Fully log user in
|
2015-03-14 01:23:43 +02:00
|
|
|
ctx.SessionStorer.Put(authboss.SessionKey, fmt.Sprintf("%s;%s", uid, provider))
|
2015-03-15 17:06:08 +02:00
|
|
|
ctx.SessionStorer.Del(authboss.SessionHalfAuthKey)
|
2015-03-13 04:20:36 +02:00
|
|
|
|
2015-03-31 21:34:03 +02:00
|
|
|
if err = authboss.a.Callbacks.FireAfter(authboss.EventOAuth, ctx); err != nil {
|
2015-03-14 01:23:43 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-03-25 04:39:20 +02:00
|
|
|
ctx.SessionStorer.Del(authboss.SessionOAuth2Params)
|
|
|
|
|
2015-03-31 21:34:03 +02:00
|
|
|
redirect := authboss.a.AuthLoginOKPath
|
2015-03-25 04:39:20 +02:00
|
|
|
query := make(url.Values)
|
|
|
|
for k, v := range values {
|
|
|
|
switch k {
|
|
|
|
case authboss.CookieRemember:
|
|
|
|
case authboss.FormValueRedirect:
|
|
|
|
redirect = v
|
|
|
|
default:
|
|
|
|
query.Set(k, v)
|
2015-03-14 01:23:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-25 04:39:20 +02:00
|
|
|
if len(query) > 0 {
|
|
|
|
redirect = fmt.Sprintf("%s?%s", redirect, query.Encode())
|
2015-03-13 04:20:36 +02:00
|
|
|
}
|
|
|
|
|
2015-03-28 17:53:32 +02:00
|
|
|
sf := fmt.Sprintf("Logged in successfully with %s.", strings.Title(provider))
|
2015-03-28 18:08:05 +02:00
|
|
|
response.Redirect(ctx, w, r, redirect, sf, "", false)
|
2015-03-13 04:20:36 +02:00
|
|
|
return nil
|
|
|
|
}
|
2015-03-17 06:46:15 +02:00
|
|
|
|
|
|
|
func logout(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) error {
|
|
|
|
switch r.Method {
|
|
|
|
case "GET":
|
|
|
|
ctx.SessionStorer.Del(authboss.SessionKey)
|
|
|
|
ctx.CookieStorer.Del(authboss.CookieRemember)
|
|
|
|
ctx.SessionStorer.Del(authboss.SessionLastAction)
|
|
|
|
|
2015-03-31 21:34:03 +02:00
|
|
|
response.Redirect(ctx, w, r, authboss.a.AuthLogoutOKPath, "You have logged out", "", true)
|
2015-03-17 06:46:15 +02:00
|
|
|
default:
|
|
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|