initial code import
This commit is contained in:
parent
c6f07d54d7
commit
fb636396a3
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
google_auth_proxy
|
||||
# Go.gitignore
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
23
README.md
23
README.md
@ -28,12 +28,19 @@ individual accounts, or a whole google apps domain.
|
||||
## Usage
|
||||
|
||||
```
|
||||
./google_auth_proxy
|
||||
Usage of ./google_auth_proxy:
|
||||
-authenticated-emails-file="": authenticate against emails via file (one per line)
|
||||
-client-id="": the Google OAuth Client ID: ie: "123456.apps.googleusercontent.com"
|
||||
-client-secret="": the OAuth Client secret
|
||||
-cookie-secret="": the seed for cookie values
|
||||
-redirect-url="": the http base to redirect to. ie: https://internalapp.yourcompany.com/oauth2/callback
|
||||
-htpasswd-file="": additionally lookup basic auth in a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption
|
||||
-pass-basic-auth=true: pass basic auth information to upstream
|
||||
-upstream=[]: the http url(s) of the upstream endpoint(s). If multiple, routing is based on URL path
|
||||
```
|
||||
-client-secret="": the OAuth Client Secret
|
||||
-cookie-domain="": an optional cookie domain to force cookies to
|
||||
-cookie-secret="": the seed string for secure cookies
|
||||
-google-apps-domain="": authenticate against the given google apps domain
|
||||
-htpasswd-file="": additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption
|
||||
-http-address="0.0.0.0:4180": <addr>:<port> to listen on for HTTP clients
|
||||
-pass-basic-auth=true: pass HTTP Basic Auth information to upstream
|
||||
-redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback"
|
||||
-upstream=[]: the http url(s) of the upstream endpoint. If multiple, routing is based on path
|
||||
-version=false: print version string
|
||||
```
|
||||
|
||||
Unauthenticated requests will be redirected to `/oauth2/sign_in` to start the sign-in process.
|
||||
|
50
cookies.go
Normal file
50
cookies.go
Normal file
@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func validateCookie(cookie *http.Cookie, seed string) (string, bool) {
|
||||
// value, timestamp, sig
|
||||
parts := strings.Split(cookie.Value, "|")
|
||||
if len(parts) != 3 {
|
||||
return "", false
|
||||
}
|
||||
sig := cookieSignature(seed, cookie.Name, parts[0], parts[1])
|
||||
if parts[2] == sig {
|
||||
ts, err := strconv.Atoi(parts[1])
|
||||
if err == nil && int64(ts) > time.Now().Add(time.Duration(24)*7*time.Hour*-1).Unix() {
|
||||
// it's a valid cookie. now get the contents
|
||||
rawValue, err := base64.URLEncoding.DecodeString(parts[0])
|
||||
if err == nil {
|
||||
return string(rawValue), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func signedCookieValue(seed string, key string, value string) string {
|
||||
encodedValue := base64.URLEncoding.EncodeToString([]byte(value))
|
||||
timeStr := fmt.Sprintf("%d", time.Now().Unix())
|
||||
sig := cookieSignature(seed, key, encodedValue, timeStr)
|
||||
cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig)
|
||||
return cookieVal
|
||||
}
|
||||
|
||||
func cookieSignature(args ...string) string {
|
||||
h := hmac.New(sha1.New, []byte(args[0]))
|
||||
for _, arg := range args[1:] {
|
||||
h.Write([]byte(arg))
|
||||
}
|
||||
var b []byte
|
||||
b = h.Sum(b)
|
||||
return base64.URLEncoding.EncodeToString(b)
|
||||
}
|
55
htpasswd.go
Normal file
55
htpasswd.go
Normal file
@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/csv"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// lookup passwords in a htpasswd file
|
||||
// The entries must have been created with -s for SHA encryption
|
||||
|
||||
type HtpasswdFile struct {
|
||||
Users map[string]string
|
||||
}
|
||||
|
||||
func NewHtpasswdFile(path string) *HtpasswdFile {
|
||||
log.Printf("using htpasswd file %s", path)
|
||||
r, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Fatalf("failed opening %v, %s", path, err.Error())
|
||||
}
|
||||
csv_reader := csv.NewReader(r)
|
||||
csv_reader.Comma = ':'
|
||||
csv_reader.Comment = '#'
|
||||
csv_reader.TrimLeadingSpace = true
|
||||
|
||||
records, err := csv_reader.ReadAll()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed reading file %s", err.Error())
|
||||
}
|
||||
h := &HtpasswdFile{Users: make(map[string]string)}
|
||||
for _, record := range records {
|
||||
h.Users[record[0]] = record[1]
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *HtpasswdFile) Validate(user string, password string) bool {
|
||||
realPassword, exists := h.Users[user]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
if realPassword[:5] == "{SHA}" {
|
||||
d := sha1.New()
|
||||
d.Write([]byte(password))
|
||||
if realPassword[5:] == base64.StdEncoding.EncodeToString(d.Sum(nil)) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
log.Printf("Invalid htpasswd entry for %s. Must be a SHA entry.", user)
|
||||
}
|
||||
return false
|
||||
}
|
90
main.go
Normal file
90
main.go
Normal file
@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const VERSION = "0.0.1"
|
||||
|
||||
var (
|
||||
showVersion = flag.Bool("version", false, "print version string")
|
||||
httpAddr = flag.String("http-address", "0.0.0.0:4180", "<addr>:<port> to listen on for HTTP clients")
|
||||
redirectUrl = flag.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
|
||||
clientID = flag.String("client-id", "", "the Google OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
|
||||
clientSecret = flag.String("client-secret", "", "the OAuth Client Secret")
|
||||
passBasicAuth = flag.Bool("pass-basic-auth", true, "pass HTTP Basic Auth information to upstream")
|
||||
htpasswdFile = flag.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption")
|
||||
cookieSecret = flag.String("cookie-secret", "", "the seed string for secure cookies")
|
||||
cookieDomain = flag.String("cookie-domain", "", "an optional cookie domain to force cookies to")
|
||||
googleAppsDomain = flag.String("google-apps-domain", "", "authenticate against the given google apps domain")
|
||||
authenticatedEmailsFile = flag.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
|
||||
upstreams = StringArray{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.Var(&upstreams, "upstream", "the http url(s) of the upstream endpoint. If multiple, routing is based on path")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *showVersion {
|
||||
fmt.Printf("google_auth_proxy v%s\n", VERSION)
|
||||
return
|
||||
}
|
||||
|
||||
if len(upstreams) < 1 {
|
||||
log.Fatal("missing --upstream")
|
||||
}
|
||||
if *cookieSecret == "" {
|
||||
log.Fatal("missing --cookie-secret")
|
||||
}
|
||||
if *clientID == "" {
|
||||
log.Fatal("missing --client-id")
|
||||
}
|
||||
if *clientSecret == "" {
|
||||
log.Fatal("missing --client-secret")
|
||||
}
|
||||
|
||||
var upstreamUrls []*url.URL
|
||||
for _, u := range upstreams {
|
||||
upstreamUrl, err := url.Parse(u)
|
||||
if err != nil {
|
||||
log.Fatalf("error parsing --upstream %s", err.Error())
|
||||
}
|
||||
upstreamUrls = append(upstreamUrls, upstreamUrl)
|
||||
}
|
||||
redirectUrl, err := url.Parse(*redirectUrl)
|
||||
if err != nil {
|
||||
log.Fatalf("error parsing --redirect-url %s", err.Error())
|
||||
}
|
||||
|
||||
validator := NewValidator(*googleAppsDomain, *authenticatedEmailsFile)
|
||||
oauthproxy := NewOauthProxy(upstreamUrls, *clientID, *clientSecret, validator)
|
||||
oauthproxy.SetRedirectUrl(redirectUrl)
|
||||
if *googleAppsDomain != "" && *authenticatedEmailsFile == "" {
|
||||
oauthproxy.SignInMessage = fmt.Sprintf("using a %s email address", *googleAppsDomain)
|
||||
}
|
||||
if *htpasswdFile != "" {
|
||||
oauthproxy.HtpasswdFile = NewHtpasswdFile(*htpasswdFile)
|
||||
}
|
||||
listener, err := net.Listen("tcp", *httpAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("FATAL: listen (%s) failed - %s", *httpAddr, err.Error())
|
||||
}
|
||||
log.Printf("listening on %s", *httpAddr)
|
||||
|
||||
server := &http.Server{Handler: oauthproxy}
|
||||
err = server.Serve(listener)
|
||||
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
||||
log.Printf("ERROR: http.Serve() - %s", err.Error())
|
||||
}
|
||||
|
||||
log.Printf("HTTP: closing %s", listener.Addr().String())
|
||||
}
|
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
|
||||
}
|
16
string_array.go
Normal file
16
string_array.go
Normal file
@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type StringArray []string
|
||||
|
||||
func (a *StringArray) Set(s string) error {
|
||||
*a = append(*a, s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *StringArray) String() string {
|
||||
return fmt.Sprint(*a)
|
||||
}
|
37
templates.go
Normal file
37
templates.go
Normal file
@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
)
|
||||
|
||||
func getTemplates() *template.Template {
|
||||
t, err := template.New("foo").Parse(`{{define "sign_in.html"}}
|
||||
<html><head><title>Sign In</title></head>
|
||||
<body>
|
||||
<form method="GET" action="/oauth2/start">
|
||||
<button type="submit">Sign In w/ Google</button>
|
||||
{{.SignInMessage}}
|
||||
</form>
|
||||
</body></html>
|
||||
{{end}}`)
|
||||
if err != nil {
|
||||
log.Fatalf("failed parsing template %s", err.Error())
|
||||
}
|
||||
t, err = t.Parse(`{{define "error.html"}}
|
||||
<html><head><title>{{.Title}}</title></head>
|
||||
<body>
|
||||
<h2>{{.Title}}</h2>
|
||||
<p>{{.Message}}</p>
|
||||
<hr>
|
||||
<form method="GET" action="/oauth2/start">
|
||||
<button type="submit">Sign In w/ Google</button>
|
||||
{{.SignInMessage}}
|
||||
</form>
|
||||
</body>
|
||||
</html>{{end}}`)
|
||||
if err != nil {
|
||||
log.Fatalf("failed parsing template %s", err.Error())
|
||||
}
|
||||
return t
|
||||
}
|
46
validator.go
Normal file
46
validator.go
Normal file
@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"log"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NewValidator(domain string, usersFile string) func(string) bool {
|
||||
|
||||
validUsers := make(map[string]bool)
|
||||
emailSuffix := ""
|
||||
if domain != "" {
|
||||
emailSuffix = fmt.Sprintf("@%s", domain)
|
||||
}
|
||||
|
||||
if usersFile != "" {
|
||||
r, err := os.Open(usersFile)
|
||||
if err != nil {
|
||||
log.Fatalf("failed opening -authenticated-emails-file=%v, %s", usersFile, err.Error())
|
||||
}
|
||||
csv_reader := csv.NewReader(r)
|
||||
csv_reader.Comma = ','
|
||||
csv_reader.Comment = '#'
|
||||
csv_reader.TrimLeadingSpace = true
|
||||
records, err := csv_reader.ReadAll()
|
||||
for _, r := range records {
|
||||
validUsers[r[0]] = true
|
||||
}
|
||||
}
|
||||
|
||||
validator := func(email string) bool {
|
||||
var valid bool
|
||||
if emailSuffix != "" {
|
||||
valid = strings.HasSuffix(email, emailSuffix)
|
||||
}
|
||||
if !valid {
|
||||
_, valid = validUsers[email]
|
||||
}
|
||||
log.Printf("validating: is %s valid? %v", email, valid)
|
||||
return valid
|
||||
}
|
||||
return validator
|
||||
}
|
Loading…
Reference in New Issue
Block a user