You've already forked oauth2-proxy
mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-11-29 22:48:19 +02:00
initial code import
This commit is contained in:
302
oauthproxy.go
Normal file
302
oauthproxy.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/bitly/go-simplejson"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const signInPath = "/oauth2/sign_in"
|
||||
const oauthStartPath = "/oauth2/start"
|
||||
const oauthCallbackPath = "/oauth2/callback"
|
||||
|
||||
type OauthProxy struct {
|
||||
CookieSeed string
|
||||
CookieKey string
|
||||
Validator func(string) bool
|
||||
|
||||
redirectUrl *url.URL // the url to receive requests at
|
||||
oauthRedemptionUrl *url.URL // endpoint to redeem the code
|
||||
oauthLoginUrl *url.URL // to redirect the user to
|
||||
oauthUserInfoUrl *url.URL
|
||||
oauthScope string
|
||||
clientID string
|
||||
clientSecret string
|
||||
SignInMessage string
|
||||
HtpasswdFile *HtpasswdFile
|
||||
serveMux *http.ServeMux
|
||||
}
|
||||
|
||||
func NewOauthProxy(proxyUrls []*url.URL, clientID string, clientSecret string, validator func(string) bool) *OauthProxy {
|
||||
login, _ := url.Parse("https://accounts.google.com/o/oauth2/auth")
|
||||
redeem, _ := url.Parse("https://accounts.google.com/o/oauth2/token")
|
||||
info, _ := url.Parse("https://www.googleapis.com/oauth2/v2/userinfo")
|
||||
serveMux := http.NewServeMux()
|
||||
for _, u := range proxyUrls {
|
||||
path := u.Path
|
||||
u.Path = ""
|
||||
log.Printf("mapping %s => %s", path, u)
|
||||
serveMux.Handle(path, httputil.NewSingleHostReverseProxy(u))
|
||||
}
|
||||
return &OauthProxy{
|
||||
CookieKey: "_oauthproxy",
|
||||
CookieSeed: *cookieSecret,
|
||||
Validator: validator,
|
||||
|
||||
clientID: clientID,
|
||||
clientSecret: clientSecret,
|
||||
oauthScope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
|
||||
oauthRedemptionUrl: redeem,
|
||||
oauthLoginUrl: login,
|
||||
oauthUserInfoUrl: info,
|
||||
serveMux: serveMux,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OauthProxy) SetRedirectUrl(redirectUrl *url.URL) {
|
||||
redirectUrl.Path = oauthCallbackPath
|
||||
p.redirectUrl = redirectUrl
|
||||
}
|
||||
|
||||
func (p *OauthProxy) GetLoginURL() string {
|
||||
params := url.Values{}
|
||||
params.Add("redirect_uri", p.redirectUrl.String())
|
||||
params.Add("approval_prompt", "force")
|
||||
params.Add("scope", p.oauthScope)
|
||||
params.Add("client_id", p.clientID)
|
||||
params.Add("response_type", "code")
|
||||
return fmt.Sprintf("%s?%s", p.oauthLoginUrl, params.Encode())
|
||||
}
|
||||
|
||||
func apiRequest(req *http.Request) (*simplejson.Json, error) {
|
||||
httpclient := &http.Client{}
|
||||
resp, err := httpclient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
log.Printf("got response code %d - %s", resp.StatusCode, body)
|
||||
return nil, errors.New("api request returned 200 status code")
|
||||
}
|
||||
data, err := simplejson.NewJson(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (p *OauthProxy) redeemCode(code string) (string, error) {
|
||||
|
||||
params := url.Values{}
|
||||
params.Add("redirect_uri", p.redirectUrl.String())
|
||||
params.Add("client_id", p.clientID)
|
||||
params.Add("client_secret", p.clientSecret)
|
||||
params.Add("code", code)
|
||||
params.Add("grant_type", "authorization_code")
|
||||
log.Printf("body is %s", params.Encode())
|
||||
req, err := http.NewRequest("POST", p.oauthRedemptionUrl.String(), bytes.NewBufferString(params.Encode()))
|
||||
if err != nil {
|
||||
log.Printf("failed building request %s", err.Error())
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
json, err := apiRequest(req)
|
||||
if err != nil {
|
||||
log.Printf("failed making request %s", err.Error())
|
||||
return "", err
|
||||
}
|
||||
access_token, err := json.Get("access_token").String()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return access_token, nil
|
||||
}
|
||||
func (p *OauthProxy) getUserInfo(token string) (string, error) {
|
||||
params := url.Values{}
|
||||
params.Add("access_token", token)
|
||||
endpoint := fmt.Sprintf("%s?%s", p.oauthUserInfoUrl.String(), params.Encode())
|
||||
log.Printf("calling %s", endpoint)
|
||||
req, err := http.NewRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
log.Printf("failed building request %s", err.Error())
|
||||
return "", err
|
||||
}
|
||||
json, err := apiRequest(req)
|
||||
if err != nil {
|
||||
log.Printf("failed making request %s", err.Error())
|
||||
return "", err
|
||||
}
|
||||
email, err := json.Get("email").String()
|
||||
if err != nil {
|
||||
log.Printf("failed getting email from response %s", err.Error())
|
||||
return "", err
|
||||
}
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func ClearCookie(rw http.ResponseWriter, req *http.Request, key string) {
|
||||
domain := strings.Split(req.Host, ":")[0]
|
||||
if *cookieDomain != "" {
|
||||
domain = *cookieDomain
|
||||
}
|
||||
cookie := &http.Cookie{
|
||||
Name: key,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Domain: domain,
|
||||
Expires: time.Now().Add(time.Duration(1) * time.Hour * -1),
|
||||
HttpOnly: true,
|
||||
}
|
||||
http.SetCookie(rw, cookie)
|
||||
}
|
||||
|
||||
func ErrorPage(rw http.ResponseWriter, code int, title string, message string, signinmessage string) {
|
||||
log.Printf("ErrorPage %d %s %s %s", code, title, message, signinmessage)
|
||||
rw.WriteHeader(code)
|
||||
t := getTemplates()
|
||||
p := struct {
|
||||
Title string
|
||||
Message string
|
||||
SignInMessage string
|
||||
}{
|
||||
Title: fmt.Sprintf("%d %s", code, title),
|
||||
Message: message,
|
||||
SignInMessage: signinmessage,
|
||||
}
|
||||
t.ExecuteTemplate(rw, "error.html", p)
|
||||
}
|
||||
|
||||
func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
// check if this is a redirect back at the end of oauth
|
||||
if req.URL.Path == signInPath {
|
||||
ClearCookie(rw, req, p.CookieKey)
|
||||
t := getTemplates()
|
||||
p := struct{ SignInMessage string }{SignInMessage: p.SignInMessage}
|
||||
t.ExecuteTemplate(rw, "sign_in.html", p)
|
||||
return
|
||||
}
|
||||
if req.URL.Path == oauthStartPath {
|
||||
http.Redirect(rw, req, p.GetLoginURL(), 302)
|
||||
return
|
||||
}
|
||||
if req.URL.Path == oauthCallbackPath {
|
||||
// finish the oauth cycle
|
||||
reqParams, err := url.ParseQuery(req.URL.RawQuery)
|
||||
if err != nil {
|
||||
ErrorPage(rw, 500, "Internal Error", err.Error(), p.SignInMessage)
|
||||
return
|
||||
}
|
||||
errorString, ok := reqParams["error"]
|
||||
if ok && len(errorString) == 1 {
|
||||
ErrorPage(rw, 403, "Permission Denied", errorString[0], p.SignInMessage)
|
||||
return
|
||||
}
|
||||
code, ok := reqParams["code"]
|
||||
if !ok || len(code) != 1 {
|
||||
ErrorPage(rw, 500, "Internal Error", "Invalid API response", p.SignInMessage)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := p.redeemCode(code[0])
|
||||
if err != nil {
|
||||
log.Printf("error redeeming code %s", err.Error())
|
||||
ErrorPage(rw, 500, "Internal Error", err.Error(), p.SignInMessage)
|
||||
return
|
||||
}
|
||||
// validate user
|
||||
email, err := p.getUserInfo(token)
|
||||
if err != nil {
|
||||
log.Printf("error redeeming code %s", err.Error())
|
||||
ErrorPage(rw, 500, "Internal Error", err.Error(), p.SignInMessage)
|
||||
return
|
||||
}
|
||||
|
||||
// set cookie, or deny
|
||||
if p.Validator(email) {
|
||||
log.Printf("authenticating %s completed", email)
|
||||
domain := strings.Split(req.Host, ":")[0]
|
||||
if *cookieDomain != "" {
|
||||
domain = *cookieDomain
|
||||
}
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: p.CookieKey,
|
||||
Value: signedCookieValue(p.CookieSeed, p.CookieKey, email),
|
||||
Path: "/",
|
||||
Domain: domain,
|
||||
Expires: time.Now().Add(time.Duration(168) * time.Hour), // 7 days
|
||||
HttpOnly: true,
|
||||
// Secure: req. ... ? set if X-Scheme: https ?
|
||||
}
|
||||
http.SetCookie(rw, cookie)
|
||||
http.Redirect(rw, req, "/", 302)
|
||||
return
|
||||
} else {
|
||||
ErrorPage(rw, 403, "Permission Denied", "Invalid Account", p.SignInMessage)
|
||||
return
|
||||
}
|
||||
}
|
||||
cookie, err := req.Cookie(p.CookieKey)
|
||||
var ok bool
|
||||
var email string
|
||||
var user string
|
||||
if err == nil {
|
||||
email, ok = validateCookie(cookie, p.CookieSeed)
|
||||
user = strings.Split(email, "@")[0]
|
||||
}
|
||||
|
||||
if !ok {
|
||||
user, ok = p.CheckBasicAuth(req)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
log.Printf("invalid cookie. redirecting to sign in")
|
||||
// TODO: capture state for which url to redirect to at the end
|
||||
http.Redirect(rw, req, "/oauth2/sign_in", 302)
|
||||
return
|
||||
}
|
||||
|
||||
// At this point, the user is authenticated. proxy normally
|
||||
if *passBasicAuth {
|
||||
req.SetBasicAuth(user, "")
|
||||
req.Header["X-Forwarded-User"] = []string{user}
|
||||
}
|
||||
|
||||
p.serveMux.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func (p *OauthProxy) CheckBasicAuth(req *http.Request) (string, bool) {
|
||||
if p.HtpasswdFile == nil {
|
||||
return "", false
|
||||
}
|
||||
s := strings.SplitN(req.Header.Get("Authorization"), " ", 2)
|
||||
if len(s) != 2 || s[0] != "Basic" {
|
||||
return "", false
|
||||
}
|
||||
b, err := base64.StdEncoding.DecodeString(s[1])
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
pair := strings.SplitN(string(b), ":", 2)
|
||||
if len(pair) != 2 {
|
||||
return "", false
|
||||
}
|
||||
if p.HtpasswdFile.Validate(pair[0], pair[1]) {
|
||||
return pair[0], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
Reference in New Issue
Block a user