diff --git a/bouquins/auth.go b/bouquins/auth.go index ddbae08..6902397 100644 --- a/bouquins/auth.go +++ b/bouquins/auth.go @@ -12,12 +12,68 @@ import ( ) const ( - alphanums = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - sessionName = "bouquins" - sessionOAuthState = "oauthState" - sessionUser = "username" + alphanums = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + sessionName = "bouquins" + sessionOAuthState = "oauthState" + sessionOAuthProvider = "provider" + sessionUser = "username" + + pProvider = "provider" ) +var ( + Providers = [1]OAuth2Provider{GithubProvider("github")} +) + +type OAuth2Provider interface { + GetUser(token *oauth2.Token) (string, error) + Name() string +} + +type GithubProvider string + +func (p GithubProvider) Name() string { + return string(p) +} +func (p GithubProvider) GetUser(token *oauth2.Token) (string, error) { + apiReq, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil) + apiReq.Header.Add("Accept", "application/vnd.github.v3+json") + apiReq.Header.Add("Authorization", "token "+token.AccessToken) + client := &http.Client{} + response, err := client.Do(apiReq) + defer response.Body.Close() + if err != nil { + log.Println("Auth error", err) + return "", fmt.Errorf("Authentification error") + } + + dec := json.NewDecoder(response.Body) + var emails []GitHubEmail + err = dec.Decode(&emails) + if err != nil { + log.Println("Error reading github API response", err) + return "", fmt.Errorf("Error reading github API response") + } + fmt.Printf("Content: %s\n", emails) + var userEmail string + for _, email := range emails { + if email.Primary && email.Verified { + userEmail = email.Email + } + } + log.Println("User email:", userEmail) + return userEmail, nil +} + +func findProvider(name string) OAuth2Provider { + for _, p := range Providers { + if p.Name() == name { + return p + } + } + return nil +} + // generates a 16 characters long random string func securedRandString() string { b := make([]byte, 16) @@ -51,12 +107,18 @@ func (app *Bouquins) SessionSet(name string, value string, res http.ResponseWrit // LoginPage redirects to OAuth login page (github) func (app *Bouquins) LoginPage(res http.ResponseWriter, req *http.Request) error { - // TODO choose provider - state := securedRandString() - app.SessionSet(sessionOAuthState, state, res, req) - url := app.OAuthConf.AuthCodeURL(state) - http.Redirect(res, req, url, http.StatusTemporaryRedirect) - return nil + provider := req.URL.Query().Get(pProvider) + oauth := app.OAuthConf[provider] + if oauth != nil { + app.SessionSet(sessionOAuthProvider, provider, res, req) + state := securedRandString() + app.SessionSet(sessionOAuthState, state, res, req) + url := oauth.AuthCodeURL(state) + http.Redirect(res, req, url, http.StatusTemporaryRedirect) + return nil + } + // choose provider + return app.render(res, tplProvider, app.NewModel("Authentification", "provider", req)) } // LogoutPage logout connected user @@ -68,45 +130,31 @@ func (app *Bouquins) LogoutPage(res http.ResponseWriter, req *http.Request) erro // CallbackPage handle OAuth 2 callback func (app *Bouquins) CallbackPage(res http.ResponseWriter, req *http.Request) error { savedState := app.Session(req).Values[sessionOAuthState] - if savedState == "" { - return fmt.Errorf("missing saved oauth state") + providerParam := app.Session(req).Values[sessionOAuthProvider] + if savedState == "" || providerParam == "" { + return fmt.Errorf("missing oauth data") + } + providerName := providerParam.(string) + oauth := app.OAuthConf[providerName] + provider := findProvider(providerName) + if oauth == nil || provider == nil { + return fmt.Errorf("missing oauth configuration") } app.SessionSet(sessionOAuthState, "", res, req) + app.SessionSet(sessionOAuthProvider, "", res, req) state := req.FormValue("state") if state != savedState { return fmt.Errorf("invalid oauth state, expected '%s', got '%s'", "state", state) } code := req.FormValue("code") - token, err := app.OAuthConf.Exchange(oauth2.NoContext, code) + token, err := oauth.Exchange(oauth2.NoContext, code) if err != nil { return fmt.Errorf("Code exchange failed with '%s'", err) } - apiReq, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil) - apiReq.Header.Add("Accept", "application/vnd.github.v3+json") - apiReq.Header.Add("Authorization", "token "+token.AccessToken) - client := &http.Client{} - response, err := client.Do(apiReq) - defer response.Body.Close() + userEmail, err := provider.GetUser(token) if err != nil { - log.Println("Auth error", err) - return fmt.Errorf("Authentification error") + return err } - - dec := json.NewDecoder(response.Body) - var emails []GitHubEmail - err = dec.Decode(&emails) - if err != nil { - log.Println("Error reading github API response", err) - return fmt.Errorf("Error reading github API response") - } - fmt.Printf("Content: %s\n", emails) - var userEmail string - for _, email := range emails { - if email.Primary && email.Verified { - userEmail = email.Email - } - } - log.Println("User email:", userEmail) // FIXME list allowed users if userEmail == "meutel+github@meutel.net" { app.SessionSet(sessionUser, "Meutel", res, req) diff --git a/bouquins/bouquins.go b/bouquins/bouquins.go index 09af64a..72373d2 100644 --- a/bouquins/bouquins.go +++ b/bouquins/bouquins.go @@ -21,12 +21,13 @@ import ( const ( Version = "master" - tplBooks = "book.html" - tplAuthors = "author.html" - tplSeries = "series.html" - tplIndex = "index.html" - tplSearch = "search.html" - tplAbout = "about.html" + tplBooks = "book.html" + tplAuthors = "author.html" + tplSeries = "series.html" + tplIndex = "index.html" + tplSearch = "search.html" + tplAbout = "about.html" + tplProvider = "provider.html" pList = "list" pOrder = "order" @@ -74,7 +75,7 @@ type GitHubEmail struct { type Bouquins struct { Tpl *template.Template DB *sql.DB - OAuthConf *oauth2.Config + OAuthConf map[string]*oauth2.Config Cookies *sessions.CookieStore } diff --git a/main.go b/main.go index e945814..ce2c35f 100644 --- a/main.go +++ b/main.go @@ -71,7 +71,8 @@ func initApp() *BouquinsConf { } // TODO conf by provider, client ID and secret in conf file - oauthConf := &oauth2.Config{ + oauthConf := make(map[string]*oauth2.Config) + oauthConf["github"] = &oauth2.Config{ ClientID: "8b0aedf07828f06918a0", ClientSecret: "eb26ec9c986fc28bd169bdddf169b794861e0d65", Scopes: []string{"user:email"}, diff --git a/templates/provider.html b/templates/provider.html new file mode 100644 index 0000000..c21ebf5 --- /dev/null +++ b/templates/provider.html @@ -0,0 +1,9 @@ +{{ template "header.html" . }} +
+

Veuillez sélectionner un mode d'authentification:

+

+
+{{ template "footer.html" . }}