Oauth2 consumer (#679)

* initial stuff for oauth2 login, fails on:
* login button on the signIn page to start the OAuth2 flow and a callback for each provider
Only GitHub is implemented for now
* show login button only when the OAuth2 consumer is configured (and activated)
* create macaron group for oauth2 urls
* prevent net/http in modules (other then oauth2)
* use a new data sessions oauth2 folder for storing the oauth2 session data
* add missing 2FA when this is enabled on the user
* add password option for OAuth2 user , for use with git over http and login to the GUI
* add tip for registering a GitHub OAuth application
* at startup of Gitea register all configured providers and also on adding/deleting of new providers
* custom handling of errors in oauth2 request init + show better tip
* add ExternalLoginUser model and migration script to add it to database
* link a external account to an existing account (still need to handle wrong login and signup) and remove if user is removed
* remove the linked external account from the user his settings
* if user is unknown we allow him to register a new account or link it to some existing account
* sign up with button on signin page (als change OAuth2Provider structure so we can store basic stuff about providers)

* from gorilla/sessions docs:
"Important Note: If you aren't using gorilla/mux, you need to wrap your handlers with context.ClearHandler as or else you will leak memory!"
(we're using gorilla/sessions for storing oauth2 sessions)

* use updated goth lib that now supports getting the OAuth2 user if the AccessToken is still valid instead of re-authenticating (prevent flooding the OAuth2 provider)
This commit is contained in:
Willem van Dreumel 2017-02-22 08:14:37 +01:00 committed by Kim "BKC" Carlbäcker
parent fd941db246
commit 01d957677f
76 changed files with 7275 additions and 137 deletions

22
vendor/github.com/markbates/goth/LICENSE.txt generated vendored Normal file
View file

@ -0,0 +1,22 @@
Copyright (c) 2014 Mark Bates
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

143
vendor/github.com/markbates/goth/README.md generated vendored Normal file
View file

@ -0,0 +1,143 @@
# Goth: Multi-Provider Authentication for Go [![GoDoc](https://godoc.org/github.com/markbates/goth?status.svg)](https://godoc.org/github.com/markbates/goth) [![Build Status](https://travis-ci.org/markbates/goth.svg)](https://travis-ci.org/markbates/goth)
Package goth provides a simple, clean, and idiomatic way to write authentication
packages for Go web applications.
Unlike other similar packages, Goth, lets you write OAuth, OAuth2, or any other
protocol providers, as long as they implement the `Provider` and `Session` interfaces.
This package was inspired by [https://github.com/intridea/omniauth](https://github.com/intridea/omniauth).
## Installation
```text
$ go get github.com/markbates/goth
```
## Supported Providers
* Amazon
* Auth0
* Bitbucket
* Box
* Cloud Foundry
* Dailymotion
* Deezer
* Digital Ocean
* Discord
* Dropbox
* Facebook
* Fitbit
* GitHub
* Gitlab
* Google+
* Heroku
* InfluxCloud
* Instagram
* Intercom
* Lastfm
* Linkedin
* Meetup
* OneDrive
* OpenID Connect (auto discovery)
* Paypal
* SalesForce
* Slack
* Soundcloud
* Spotify
* Steam
* Stripe
* Twitch
* Twitter
* Uber
* Wepay
* Yahoo
* Yammer
## Examples
See the [examples](examples) folder for a working application that lets users authenticate
through Twitter, Facebook, Google Plus etc.
To run the example either clone the source from GitHub
```text
$ git clone git@github.com:markbates/goth.git
```
or use
```text
$ go get github.com/markbates/goth
```
```text
$ cd goth/examples
$ go get -v
$ go build
$ ./examples
```
Now open up your browser and go to [http://localhost:3000](http://localhost:3000) to see the example.
To actually use the different providers, please make sure you configure them given the system environments as defined in the examples/main.go file
## Issues
Issues always stand a significantly better chance of getting fixed if the are accompanied by a
pull request.
## Contributing
Would I love to see more providers? Certainly! Would you love to contribute one? Hopefully, yes!
1. Fork it
2. Create your feature branch (git checkout -b my-new-feature)
3. Write Tests!
4. Commit your changes (git commit -am 'Add some feature')
5. Push to the branch (git push origin my-new-feature)
6. Create new Pull Request
## Contributors
* Mark Bates
* Tyler Bunnell
* Corey McGrillis
* willemvd
* Rakesh Goyal
* Andy Grunwald
* Glenn Walker
* Kevin Fitzpatrick
* Ben Tranter
* Sharad Ganapathy
* Andrew Chilton
* sharadgana
* Aurorae
* Craig P Jolicoeur
* Zac Bergquist
* Geoff Franks
* Raphael Geronimi
* Noah Shibley
* lumost
* oov
* Felix Lamouroux
* Rafael Quintela
* Tyler
* DenSm
* Samy KACIMI
* dante gray
* Noah
* Jacob Walker
* Marin Martinic
* Roy
* Omni Adams
* Sasa Brankovic
* dkhamsing
* Dante Swift
* Attila Domokos
* Albin Gilles
* Syed Zubairuddin
* Johnny Boursiquot
* Jerome Touffe-Blin
* bryanl
* Masanobu YOSHIOKA
* Jonathan Hall
* HaiMing.Yin
* Sairam Kunala

10
vendor/github.com/markbates/goth/doc.go generated vendored Normal file
View file

@ -0,0 +1,10 @@
/*
Package goth provides a simple, clean, and idiomatic way to write authentication
packages for Go web applications.
This package was inspired by https://github.com/intridea/omniauth.
See the examples folder for a working application that lets users authenticate
through Twitter or Facebook.
*/
package goth

219
vendor/github.com/markbates/goth/gothic/gothic.go generated vendored Normal file
View file

@ -0,0 +1,219 @@
/*
Package gothic wraps common behaviour when using Goth. This makes it quick, and easy, to get up
and running with Goth. Of course, if you want complete control over how things flow, in regards
to the authentication process, feel free and use Goth directly.
See https://github.com/markbates/goth/examples/main.go to see this in action.
*/
package gothic
import (
"errors"
"fmt"
"net/http"
"os"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/markbates/goth"
)
// SessionName is the key used to access the session store.
const SessionName = "_gothic_session"
// Store can/should be set by applications using gothic. The default is a cookie store.
var Store sessions.Store
var defaultStore sessions.Store
var keySet = false
func init() {
key := []byte(os.Getenv("SESSION_SECRET"))
keySet = len(key) != 0
Store = sessions.NewCookieStore([]byte(key))
defaultStore = Store
}
/*
BeginAuthHandler is a convienence handler for starting the authentication process.
It expects to be able to get the name of the provider from the query parameters
as either "provider" or ":provider".
BeginAuthHandler will redirect the user to the appropriate authentication end-point
for the requested provider.
See https://github.com/markbates/goth/examples/main.go to see this in action.
*/
func BeginAuthHandler(res http.ResponseWriter, req *http.Request) {
url, err := GetAuthURL(res, req)
if err != nil {
res.WriteHeader(http.StatusBadRequest)
fmt.Fprintln(res, err)
return
}
http.Redirect(res, req, url, http.StatusTemporaryRedirect)
}
// SetState sets the state string associated with the given request.
// If no state string is associated with the request, one will be generated.
// This state is sent to the provider and can be retrieved during the
// callback.
var SetState = func(req *http.Request) string {
state := req.URL.Query().Get("state")
if len(state) > 0 {
return state
}
return "state"
}
// GetState gets the state returned by the provider during the callback.
// This is used to prevent CSRF attacks, see
// http://tools.ietf.org/html/rfc6749#section-10.12
var GetState = func(req *http.Request) string {
return req.URL.Query().Get("state")
}
/*
GetAuthURL starts the authentication process with the requested provided.
It will return a URL that should be used to send users to.
It expects to be able to get the name of the provider from the query parameters
as either "provider" or ":provider".
I would recommend using the BeginAuthHandler instead of doing all of these steps
yourself, but that's entirely up to you.
*/
func GetAuthURL(res http.ResponseWriter, req *http.Request) (string, error) {
if !keySet && defaultStore == Store {
fmt.Println("goth/gothic: no SESSION_SECRET environment variable is set. The default cookie store is not available and any calls will fail. Ignore this warning if you are using a different store.")
}
providerName, err := GetProviderName(req)
if err != nil {
return "", err
}
provider, err := goth.GetProvider(providerName)
if err != nil {
return "", err
}
sess, err := provider.BeginAuth(SetState(req))
if err != nil {
return "", err
}
url, err := sess.GetAuthURL()
if err != nil {
return "", err
}
err = storeInSession(providerName, sess.Marshal(), req, res)
if err != nil {
return "", err
}
return url, err
}
/*
CompleteUserAuth does what it says on the tin. It completes the authentication
process and fetches all of the basic information about the user from the provider.
It expects to be able to get the name of the provider from the query parameters
as either "provider" or ":provider".
See https://github.com/markbates/goth/examples/main.go to see this in action.
*/
var CompleteUserAuth = func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
if !keySet && defaultStore == Store {
fmt.Println("goth/gothic: no SESSION_SECRET environment variable is set. The default cookie store is not available and any calls will fail. Ignore this warning if you are using a different store.")
}
providerName, err := GetProviderName(req)
if err != nil {
return goth.User{}, err
}
provider, err := goth.GetProvider(providerName)
if err != nil {
return goth.User{}, err
}
value, err := getFromSession(providerName, req)
if err != nil {
return goth.User{}, err
}
sess, err := provider.UnmarshalSession(value)
if err != nil {
return goth.User{}, err
}
user, err := provider.FetchUser(sess)
if err == nil {
// user can be found with existing session data
return user, err
}
// get new token and retry fetch
_, err = sess.Authorize(provider, req.URL.Query())
if err != nil {
return goth.User{}, err
}
err = storeInSession(providerName, sess.Marshal(), req, res)
if err != nil {
return goth.User{}, err
}
return provider.FetchUser(sess)
}
// GetProviderName is a function used to get the name of a provider
// for a given request. By default, this provider is fetched from
// the URL query string. If you provide it in a different way,
// assign your own function to this variable that returns the provider
// name for your request.
var GetProviderName = getProviderName
func getProviderName(req *http.Request) (string, error) {
provider := req.URL.Query().Get("provider")
if provider == "" {
if p, ok := mux.Vars(req)["provider"]; ok {
return p, nil
}
}
if provider == "" {
provider = req.URL.Query().Get(":provider")
}
if provider == "" {
return provider, errors.New("you must select a provider")
}
return provider, nil
}
func storeInSession(key string, value string, req *http.Request, res http.ResponseWriter) error {
session, _ := Store.Get(req, key + SessionName)
session.Values[key] = value
return session.Save(req, res)
}
func getFromSession(key string, req *http.Request) (string, error) {
session, _ := Store.Get(req, key + SessionName)
value := session.Values[key]
if value == nil {
return "", errors.New("could not find a matching session for this request")
}
return value.(string), nil
}

75
vendor/github.com/markbates/goth/provider.go generated vendored Normal file
View file

@ -0,0 +1,75 @@
package goth
import (
"fmt"
"net/http"
"golang.org/x/net/context"
"golang.org/x/oauth2"
)
// Provider needs to be implemented for each 3rd party authentication provider
// e.g. Facebook, Twitter, etc...
type Provider interface {
Name() string
SetName(name string)
BeginAuth(state string) (Session, error)
UnmarshalSession(string) (Session, error)
FetchUser(Session) (User, error)
Debug(bool)
RefreshToken(refreshToken string) (*oauth2.Token, error) //Get new access token based on the refresh token
RefreshTokenAvailable() bool //Refresh token is provided by auth provider or not
}
const NoAuthUrlErrorMessage = "an AuthURL has not been set"
// Providers is list of known/available providers.
type Providers map[string]Provider
var providers = Providers{}
// UseProviders adds a list of available providers for use with Goth.
// Can be called multiple times. If you pass the same provider more
// than once, the last will be used.
func UseProviders(viders ...Provider) {
for _, provider := range viders {
providers[provider.Name()] = provider
}
}
// GetProviders returns a list of all the providers currently in use.
func GetProviders() Providers {
return providers
}
// GetProvider returns a previously created provider. If Goth has not
// been told to use the named provider it will return an error.
func GetProvider(name string) (Provider, error) {
provider := providers[name]
if provider == nil {
return nil, fmt.Errorf("no provider for %s exists", name)
}
return provider, nil
}
// ClearProviders will remove all providers currently in use.
// This is useful, mostly, for testing purposes.
func ClearProviders() {
providers = Providers{}
}
// ContextForClient provides a context for use with oauth2.
func ContextForClient(h *http.Client) context.Context {
if h == nil {
return oauth2.NoContext
}
return context.WithValue(oauth2.NoContext, oauth2.HTTPClient, h)
}
// HTTPClientWithFallBack to be used in all fetch operations.
func HTTPClientWithFallBack(h *http.Client) *http.Client {
if h != nil {
return h
}
return http.DefaultClient
}

View file

@ -0,0 +1,224 @@
// Package github implements the OAuth2 protocol for authenticating users through Github.
// This package can be used as a reference implementation of an OAuth2 provider for Goth.
package github
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/markbates/goth"
"golang.org/x/oauth2"
)
// These vars define the Authentication, Token, and API URLS for GitHub. If
// using GitHub enterprise you should change these values before calling New.
//
// Examples:
// github.AuthURL = "https://github.acme.com/login/oauth/authorize
// github.TokenURL = "https://github.acme.com/login/oauth/access_token
// github.ProfileURL = "https://github.acme.com/api/v3/user
// github.EmailURL = "https://github.acme.com/api/v3/user/emails
var (
AuthURL = "https://github.com/login/oauth/authorize"
TokenURL = "https://github.com/login/oauth/access_token"
ProfileURL = "https://api.github.com/user"
EmailURL = "https://api.github.com/user/emails"
)
// New creates a new Github provider, and sets up important connection details.
// You should always call `github.New` to get a new Provider. Never try to create
// one manually.
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
providerName: "github",
}
p.config = newConfig(p, scopes)
return p
}
// Provider is the implementation of `goth.Provider` for accessing Github.
type Provider struct {
ClientKey string
Secret string
CallbackURL string
HTTPClient *http.Client
config *oauth2.Config
providerName string
}
// Name is the name used to retrieve this provider later.
func (p *Provider) Name() string {
return p.providerName
}
// SetName is to update the name of the provider (needed in case of multiple providers of 1 type)
func (p *Provider) SetName(name string) {
p.providerName = name
}
func (p *Provider) Client() *http.Client {
return goth.HTTPClientWithFallBack(p.HTTPClient)
}
// Debug is a no-op for the github package.
func (p *Provider) Debug(debug bool) {}
// BeginAuth asks Github for an authentication end-point.
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
url := p.config.AuthCodeURL(state)
session := &Session{
AuthURL: url,
}
return session, nil
}
// FetchUser will go to Github and access basic information about the user.
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
sess := session.(*Session)
user := goth.User{
AccessToken: sess.AccessToken,
Provider: p.Name(),
}
if user.AccessToken == "" {
// data is not yet retrieved since accessToken is still empty
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
}
response, err := p.Client().Get(ProfileURL + "?access_token=" + url.QueryEscape(sess.AccessToken))
if err != nil {
return user, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return user, fmt.Errorf("GitHub API responded with a %d trying to fetch user information", response.StatusCode)
}
bits, err := ioutil.ReadAll(response.Body)
if err != nil {
return user, err
}
err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData)
if err != nil {
return user, err
}
err = userFromReader(bytes.NewReader(bits), &user)
if err != nil {
return user, err
}
if user.Email == "" {
for _, scope := range p.config.Scopes {
if strings.TrimSpace(scope) == "user" || strings.TrimSpace(scope) == "user:email" {
user.Email, err = getPrivateMail(p, sess)
if err != nil {
return user, err
}
break
}
}
}
return user, err
}
func userFromReader(reader io.Reader, user *goth.User) error {
u := struct {
ID int `json:"id"`
Email string `json:"email"`
Bio string `json:"bio"`
Name string `json:"name"`
Login string `json:"login"`
Picture string `json:"avatar_url"`
Location string `json:"location"`
}{}
err := json.NewDecoder(reader).Decode(&u)
if err != nil {
return err
}
user.Name = u.Name
user.NickName = u.Login
user.Email = u.Email
user.Description = u.Bio
user.AvatarURL = u.Picture
user.UserID = strconv.Itoa(u.ID)
user.Location = u.Location
return err
}
func getPrivateMail(p *Provider, sess *Session) (email string, err error) {
response, err := p.Client().Get(EmailURL + "?access_token=" + url.QueryEscape(sess.AccessToken))
if err != nil {
if response != nil {
response.Body.Close()
}
return email, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return email, fmt.Errorf("GitHub API responded with a %d trying to fetch user email", response.StatusCode)
}
var mailList = []struct {
Email string `json:"email"`
Primary bool `json:"primary"`
Verified bool `json:"verified"`
}{}
err = json.NewDecoder(response.Body).Decode(&mailList)
if err != nil {
return email, err
}
for _, v := range mailList {
if v.Primary && v.Verified {
return v.Email, nil
}
}
// can't get primary email - shouldn't be possible
return
}
func newConfig(provider *Provider, scopes []string) *oauth2.Config {
c := &oauth2.Config{
ClientID: provider.ClientKey,
ClientSecret: provider.Secret,
RedirectURL: provider.CallbackURL,
Endpoint: oauth2.Endpoint{
AuthURL: AuthURL,
TokenURL: TokenURL,
},
Scopes: []string{},
}
for _, scope := range scopes {
c.Scopes = append(c.Scopes, scope)
}
return c
}
//RefreshToken refresh token is not provided by github
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
return nil, errors.New("Refresh token is not provided by github")
}
//RefreshTokenAvailable refresh token is not provided by github
func (p *Provider) RefreshTokenAvailable() bool {
return false
}

View file

@ -0,0 +1,56 @@
package github
import (
"encoding/json"
"errors"
"strings"
"github.com/markbates/goth"
)
// Session stores data during the auth process with Github.
type Session struct {
AuthURL string
AccessToken string
}
// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Github provider.
func (s Session) GetAuthURL() (string, error) {
if s.AuthURL == "" {
return "", errors.New(goth.NoAuthUrlErrorMessage)
}
return s.AuthURL, nil
}
// Authorize the session with Github and return the access token to be stored for future use.
func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) {
p := provider.(*Provider)
token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code"))
if err != nil {
return "", err
}
if !token.Valid() {
return "", errors.New("Invalid token received from provider")
}
s.AccessToken = token.AccessToken
return token.AccessToken, err
}
// Marshal the session into a string
func (s Session) Marshal() string {
b, _ := json.Marshal(s)
return string(b)
}
func (s Session) String() string {
return s.Marshal()
}
// UnmarshalSession will unmarshal a JSON string into a session.
func (p *Provider) UnmarshalSession(data string) (goth.Session, error) {
sess := &Session{}
err := json.NewDecoder(strings.NewReader(data)).Decode(sess)
return sess, err
}

21
vendor/github.com/markbates/goth/session.go generated vendored Normal file
View file

@ -0,0 +1,21 @@
package goth
// Params is used to pass data to sessions for authorization. An existing
// implementation, and the one most likely to be used, is `url.Values`.
type Params interface {
Get(string) string
}
// Session needs to be implemented as part of the provider package.
// It will be marshaled and persisted between requests to "tie"
// the start and the end of the authorization process with a
// 3rd party provider.
type Session interface {
// GetAuthURL returns the URL for the authentication end-point for the provider.
GetAuthURL() (string, error)
// Marshal generates a string representation of the Session for storing between requests.
Marshal() string
// Authorize should validate the data from the provider and return an access token
// that can be stored for later access to the provider.
Authorize(Provider, Params) (string, error)
}

30
vendor/github.com/markbates/goth/user.go generated vendored Normal file
View file

@ -0,0 +1,30 @@
package goth
import (
"encoding/gob"
"time"
)
func init() {
gob.Register(User{})
}
// User contains the information common amongst most OAuth and OAuth2 providers.
// All of the "raw" datafrom the provider can be found in the `RawData` field.
type User struct {
RawData map[string]interface{}
Provider string
Email string
Name string
FirstName string
LastName string
NickName string
Description string
UserID string
AvatarURL string
Location string
AccessToken string
AccessTokenSecret string
RefreshToken string
ExpiresAt time.Time
}