Skip to content

Commit

Permalink
Add support for openid with github and facebook
Browse files Browse the repository at this point in the history
  • Loading branch information
xetorthio committed Oct 4, 2017
1 parent eebe638 commit 4c03481
Show file tree
Hide file tree
Showing 25 changed files with 712 additions and 251 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
play-with-docker
node_modules
docker-compose.single.yml
lala
51 changes: 50 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import (
"os"
"regexp"
"time"

"github.com/gorilla/securecookie"

"golang.org/x/oauth2"
oauth2FB "golang.org/x/oauth2/facebook"
oauth2Github "golang.org/x/oauth2/github"
)

const (
Expand All @@ -21,12 +27,17 @@ const (
var NameFilter = regexp.MustCompile(PWDHostPortGroupRegex)
var AliasFilter = regexp.MustCompile(AliasPortGroupRegex)

var PortNumber, Key, Cert, SessionsFile, PWDContainerName, L2ContainerName, L2Subdomain, PWDCName, HashKey, SSHKeyPath, L2RouterIP, DindVolumeSize string
var PortNumber, Key, Cert, SessionsFile, PWDContainerName, L2ContainerName, L2Subdomain, PWDCName, HashKey, SSHKeyPath, L2RouterIP, DindVolumeSize, CookieHashKey, CookieBlockKey string
var UseLetsEncrypt, ExternalDindVolume, NoWindows bool
var LetsEncryptCertsDir string
var LetsEncryptDomains stringslice
var MaxLoadAvg float64
var ForceTLS bool
var Providers map[string]*oauth2.Config
var SecureCookie *securecookie.SecureCookie

var GithubClientID, GithubClientSecret string
var FacebookClientID, FacebookClientSecret string

type stringslice []string

Expand Down Expand Up @@ -58,8 +69,46 @@ func ParseFlags() {
flag.BoolVar(&ExternalDindVolume, "external-dind-volume", false, "Use external dind volume")
flag.Float64Var(&MaxLoadAvg, "maxload", 100, "Maximum allowed load average before failing ping requests")
flag.StringVar(&SSHKeyPath, "ssh_key_path", "", "SSH Private Key to use")
flag.StringVar(&CookieHashKey, "cookie-hash-key", "", "Hash key to use to validate cookies")
flag.StringVar(&CookieBlockKey, "cookie-block-key", "", "Block key to use to encrypt cookies")

flag.StringVar(&GithubClientID, "github-client-id", "", "Github OAuth Client ID")
flag.StringVar(&GithubClientSecret, "github-client-secret", "", "Github OAuth Client Secret")

flag.StringVar(&FacebookClientID, "facebook-client-id", "", "Facebook OAuth Client ID")
flag.StringVar(&FacebookClientSecret, "facebook-client-secret", "", "Facebook OAuth Client Secret")

flag.Parse()

SecureCookie = securecookie.New([]byte(CookieHashKey), []byte(CookieBlockKey))

registerOAuthProviders()
}

func registerOAuthProviders() {
Providers = map[string]*oauth2.Config{}
if GithubClientID != "" && GithubClientSecret != "" {
conf := &oauth2.Config{
ClientID: GithubClientID,
ClientSecret: GithubClientSecret,
Scopes: []string{"user:email"},
Endpoint: oauth2Github.Endpoint,
}

Providers["github"] = conf
}
if FacebookClientID != "" && FacebookClientSecret != "" {
conf := &oauth2.Config{
ClientID: FacebookClientID,
ClientSecret: FacebookClientSecret,
Scopes: []string{"email", "public_profile"},
Endpoint: oauth2FB.Endpoint,
}

Providers["facebook"] = conf
}
}

func GetDindImageName() string {
dindImage := os.Getenv("DIND_IMAGE")
defaultDindImageName := "franela/dind"
Expand Down
19 changes: 5 additions & 14 deletions handlers/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"log"
"net/http"
"os"
"time"

"golang.org/x/crypto/acme/autocert"
Expand All @@ -16,7 +15,6 @@ import (
"github.com/play-with-docker/play-with-docker/config"
"github.com/play-with-docker/play-with-docker/event"
"github.com/play-with-docker/play-with-docker/pwd"
"github.com/play-with-docker/play-with-docker/templates"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/urfave/negroni"
)
Expand All @@ -33,9 +31,6 @@ func Bootstrap(c pwd.PWDApi, ev event.EventApi) {
}

func Register(extend HandlerExtender) {

bypassCaptcha := len(os.Getenv("GOOGLE_RECAPTCHA_DISABLED")) > 0

server, err := socketio.NewServer(nil)
if err != nil {
log.Fatal(err)
Expand Down Expand Up @@ -81,17 +76,13 @@ func Register(extend HandlerExtender) {

// Generic routes
r.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
if bypassCaptcha {
http.ServeFile(rw, r, "./www/bypass.html")
} else {
welcome, tmplErr := templates.GetWelcomeTemplate()
if tmplErr != nil {
log.Fatal(tmplErr)
}
rw.Write(welcome)
}
http.ServeFile(rw, r, "./www/landing.html")
}).Methods("GET")

r.HandleFunc("/oauth/providers", ListProviders).Methods("GET")
r.HandleFunc("/oauth/providers/{provider}/login", Login).Methods("GET")
r.HandleFunc("/oauth/providers/{provider}/callback", LoginCallback).Methods("GET")

corsRouter.HandleFunc("/", NewSession).Methods("POST")

if extend != nil {
Expand Down
40 changes: 40 additions & 0 deletions handlers/cookie_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package handlers

import (
"net/http"

"github.com/play-with-docker/play-with-docker/config"
)

type CookieID struct {
Id string `json:"id"`
UserName string `json:"user_name"`
UserAvatar string `json:"user_avatar"`
}

func (c *CookieID) SetCookie(rw http.ResponseWriter) error {
if encoded, err := config.SecureCookie.Encode("id", c); err == nil {
cookie := &http.Cookie{
Name: "id",
Value: encoded,
Path: "/",
Secure: config.UseLetsEncrypt,
}
http.SetCookie(rw, cookie)
} else {
return err
}
return nil
}
func ReadCookie(r *http.Request) (*CookieID, error) {
if cookie, err := r.Cookie("id"); err == nil {
value := &CookieID{}
if err = config.SecureCookie.Decode("id", cookie.Value, &value); err == nil {
return value, nil
} else {
return nil, err
}
} else {
return nil, err
}
}
146 changes: 146 additions & 0 deletions handlers/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package handlers

import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"

"golang.org/x/oauth2"

"github.com/google/go-github/github"
"github.com/gorilla/mux"
fb "github.com/huandu/facebook"
"github.com/play-with-docker/play-with-docker/config"
"github.com/play-with-docker/play-with-docker/pwd/types"
)

func ListProviders(rw http.ResponseWriter, req *http.Request) {
providers := []string{}
for name, _ := range config.Providers {
providers = append(providers, name)
}
json.NewEncoder(rw).Encode(providers)
}

func Login(rw http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
providerName := vars["provider"]

provider, found := config.Providers[providerName]
if !found {
log.Printf("Could not find provider %s\n", providerName)
rw.WriteHeader(http.StatusNotFound)
return
}

loginRequest, err := core.UserNewLoginRequest(providerName)
if err != nil {
log.Printf("Could not start a new user login request for provider %s. Got: %v\n", providerName, err)
rw.WriteHeader(http.StatusInternalServerError)
return
}

scheme := "http"
if req.URL.Scheme != "" {
scheme = req.URL.Scheme
}
host := "localhost"
if req.URL.Host != "" {
host = req.URL.Host
}
provider.RedirectURL = fmt.Sprintf("%s://%s/oauth/providers/%s/callback", scheme, host, providerName)
url := provider.AuthCodeURL(loginRequest.Id)

http.Redirect(rw, req, url, http.StatusFound)
}

func LoginCallback(rw http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
providerName := vars["provider"]

provider, found := config.Providers[providerName]
if !found {
log.Printf("Could not find provider %s\n", providerName)
rw.WriteHeader(http.StatusNotFound)
return
}

query := req.URL.Query()

code := query.Get("code")
loginRequestId := query.Get("state")

loginRequest, err := core.UserGetLoginRequest(loginRequestId)
if err != nil {
log.Printf("Could not get login request %s for provider %s. Got: %v\n", loginRequestId, providerName, err)
rw.WriteHeader(http.StatusInternalServerError)
return
}

ctx := req.Context()
tok, err := provider.Exchange(ctx, code)
if err != nil {
log.Printf("Could not exchage code for access token for provider %s. Got: %v\n", providerName, err)
rw.WriteHeader(http.StatusInternalServerError)
return
}

user := &types.User{Provider: providerName}
if providerName == "github" {
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: tok.AccessToken},
)
tc := oauth2.NewClient(ctx, ts)
client := github.NewClient(tc)
u, _, err := client.Users.Get(ctx, "")
if err != nil {
log.Printf("Could not get user from github. Got: %v\n", err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
user.ProviderUserId = strconv.Itoa(u.GetID())
user.Name = u.GetName()
user.Avatar = u.GetAvatarURL()
user.Email = u.GetEmail()
} else if providerName == "facebook" {
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: tok.AccessToken},
)
tc := oauth2.NewClient(ctx, ts)
session := &fb.Session{
Version: "v2.10",
HttpClient: tc,
}
p := fb.Params{}
p["fields"] = "email,name,picture"
res, err := session.Get("/me", p)
if err != nil {
log.Printf("Could not get user from facebook. Got: %v\n", err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
user.ProviderUserId = res.Get("id").(string)
user.Name = res.Get("name").(string)
user.Avatar = res.Get("picture.data.url").(string)
user.Email = res.Get("email").(string)
}

user, err = core.UserLogin(loginRequest, user)
if err != nil {
log.Printf("Could not login user. Got: %v\n", err)
rw.WriteHeader(http.StatusInternalServerError)
return
}

cookieData := CookieID{Id: user.Id, UserName: user.Name, UserAvatar: user.Avatar}

if err := cookieData.SetCookie(rw); err != nil {
log.Printf("Could not encode cookie. Got: %v\n", err)
rw.WriteHeader(http.StatusInternalServerError)
return
}

http.Redirect(rw, req, "/", http.StatusFound)
}
17 changes: 11 additions & 6 deletions handlers/new_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"github.com/play-with-docker/play-with-docker/config"
"github.com/play-with-docker/play-with-docker/provisioner"
"github.com/play-with-docker/play-with-docker/recaptcha"
)

type NewSessionResponse struct {
Expand All @@ -20,10 +19,16 @@ type NewSessionResponse struct {

func NewSession(rw http.ResponseWriter, req *http.Request) {
req.ParseForm()
if !recaptcha.IsHuman(req, rw) {
// User it not a human
rw.WriteHeader(http.StatusForbidden)
return

userId := ""
if len(config.Providers) > 0 {
cookie, err := ReadCookie(req)
if err != nil {
// User it not a human
rw.WriteHeader(http.StatusForbidden)
return
}
userId = cookie.Id
}

reqDur := req.Form.Get("session-duration")
Expand All @@ -45,7 +50,7 @@ func NewSession(rw http.ResponseWriter, req *http.Request) {

}
duration := config.GetDuration(reqDur)
s, err := core.SessionNew(duration, stack, stackName, imageName)
s, err := core.SessionNew(userId, duration, stack, stackName, imageName)
if err != nil {
if provisioner.OutOfCapacity(err) {
http.Redirect(rw, req, "/ooc", http.StatusFound)
Expand Down
6 changes: 3 additions & 3 deletions pwd/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestClientNew(t *testing.T) {
p := NewPWD(_f, _e, _s, sp, ipf)
p.generator = _g

session, err := p.SessionNew(time.Hour, "", "", "")
session, err := p.SessionNew("", time.Hour, "", "", "")
assert.Nil(t, err)

client := p.ClientNew("foobar", session)
Expand Down Expand Up @@ -82,7 +82,7 @@ func TestClientCount(t *testing.T) {
p := NewPWD(_f, _e, _s, sp, ipf)
p.generator = _g

session, err := p.SessionNew(time.Hour, "", "", "")
session, err := p.SessionNew("", time.Hour, "", "", "")
assert.Nil(t, err)

p.ClientNew("foobar", session)
Expand Down Expand Up @@ -123,7 +123,7 @@ func TestClientResizeViewPort(t *testing.T) {
p := NewPWD(_f, _e, _s, sp, ipf)
p.generator = _g

session, err := p.SessionNew(time.Hour, "", "", "")
session, err := p.SessionNew("", time.Hour, "", "", "")
assert.Nil(t, err)
client := p.ClientNew("foobar", session)
_s.On("ClientFindBySessionId", "aaaabbbbcccc").Return([]*types.Client{client}, nil)
Expand Down
Loading

0 comments on commit 4c03481

Please sign in to comment.