├── wercker.yml ├── examples ├── environment └── main.go ├── .gitignore ├── NOTICE ├── README.md ├── oauth2_test.go ├── oauth2.go └── LICENSE /wercker.yml: -------------------------------------------------------------------------------- 1 | box: wercker/golang@1.1.1 -------------------------------------------------------------------------------- /examples/environment: -------------------------------------------------------------------------------- 1 | OAUTH2_CLIENT_ID=replace_with_your_client_id 2 | OAUTH2_CLIENT_SECRET=replace_with_your_client_secret 3 | OAUTH2_REDIRECT_URL=replace_with_your_redirect_url 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2014 GoIncremental Limited. All Rights Reserved. 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | 14 | This product includes software developed at 15 | Go Incremental limited (http://www.goincremental.com/). 16 | 17 | This software contains code derived from github.com/martini-contrib/oauth2 18 | Copyright 2014 Google Inc. All Rights Reserved. Licensed under Apache 2.0 19 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 GoIncremental Limited. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "net/http" 20 | "os" 21 | 22 | oauth2 "github.com/goincremental/negroni-oauth2" 23 | sessions "github.com/goincremental/negroni-sessions" 24 | "github.com/goincremental/negroni-sessions/cookiestore" 25 | "github.com/joho/godotenv" 26 | "github.com/urfave/negroni" 27 | ) 28 | 29 | func getEnv(key string, defaultValue string) string { 30 | v := os.Getenv(key) 31 | if v == "" { 32 | v = defaultValue 33 | } 34 | return v 35 | } 36 | 37 | func main() { 38 | //Loads environment variables from a .env file 39 | godotenv.Load("environment") 40 | 41 | var ( 42 | clientID = getEnv("OAUTH2_CLIENT_ID", "client_id") 43 | clientSecret = getEnv("OAUTH2_CLIENT_SECRET", "client_secret") 44 | redirectURL = getEnv("OAUTH2_REDIRECT_URL", "redirect_url") 45 | ) 46 | 47 | secureMux := http.NewServeMux() 48 | 49 | // Routes that require a logged in user 50 | // can be protected by using a separate route handler 51 | // If the user is not authenticated, they will be 52 | // redirected to the login path. 53 | secureMux.HandleFunc("/restrict", func(w http.ResponseWriter, req *http.Request) { 54 | token := oauth2.GetToken(req) 55 | fmt.Fprintf(w, "OK: %s", token.Access()) 56 | }) 57 | 58 | secure := negroni.New() 59 | secure.Use(oauth2.LoginRequired()) 60 | secure.UseHandler(secureMux) 61 | 62 | n := negroni.New() 63 | n.Use(sessions.Sessions("my_session", cookiestore.New([]byte("secret123")))) 64 | n.Use(oauth2.Google(&oauth2.Config{ 65 | ClientID: clientID, 66 | ClientSecret: clientSecret, 67 | RedirectURL: redirectURL, 68 | Scopes: []string{"https://www.googleapis.com/auth/drive"}, 69 | })) 70 | 71 | router := http.NewServeMux() 72 | 73 | //routes added to mux do not require authentication 74 | router.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 75 | token := oauth2.GetToken(req) 76 | if token == nil || !token.Valid() { 77 | fmt.Fprintf(w, "not logged in, or the access token is expired") 78 | return 79 | } 80 | fmt.Fprintf(w, "logged in") 81 | return 82 | }) 83 | 84 | //There is probably a nicer way to handle this than repeat the restricted routes again 85 | //of course, you could use something like gorilla/mux and define prefix / regex etc. 86 | router.Handle("/restrict", secure) 87 | 88 | n.UseHandler(router) 89 | 90 | n.Run(":3000") 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # negroni-oauth2 [![GoDoc](https://godoc.org/github.com/GoIncremental/negroni-oauth2?status.svg)](http://godoc.org/github.com/GoIncremental/negroni-oauth2) [![wercker status](https://app.wercker.com/status/3dec6e8ae34f8069700c83837077f115/s "wercker status")](https://app.wercker.com/project/bykey/3dec6e8ae34f8069700c83837077f115) 2 | 3 | 4 | Allows your Negroni application to support user login via an OAuth 2.0 backend. Requires [`negroni-sessions`](https://github.com/goincremental/negroni-sessions) middleware. 5 | 6 | Google, Facebook, LinkedIn and Github sign-in are currently supported. 7 | 8 | Once endpoints are provided, this middleware can work with any OAuth 2.0 backend. 9 | 10 | ## Usage 11 | 12 | ~~~ go 13 | package main 14 | 15 | import ( 16 | "fmt" 17 | "net/http" 18 | 19 | oauth2 "github.com/goincremental/negroni-oauth2" 20 | sessions "github.com/goincremental/negroni-sessions" 21 | "github.com/goincremental/negroni-sessions/cookiestore" 22 | "github.com/urfave/negroni" 23 | ) 24 | 25 | func main() { 26 | 27 | secureMux := http.NewServeMux() 28 | 29 | // Routes that require a logged in user 30 | // can be protected by using a separate route handler 31 | // If the user is not authenticated, they will be 32 | // redirected to the login path. 33 | secureMux.HandleFunc("/restrict", func(w http.ResponseWriter, req *http.Request) { 34 | token := oauth2.GetToken(req) 35 | fmt.Fprintf(w, "OK: %s", token.Access()) 36 | }) 37 | 38 | secure := negroni.New() 39 | secure.Use(oauth2.LoginRequired()) 40 | secure.UseHandler(secureMux) 41 | 42 | n := negroni.New() 43 | n.Use(sessions.Sessions("my_session", cookiestore.New([]byte("secret123")))) 44 | n.Use(oauth2.Google(&oauth2.Config{ 45 | ClientID: "client_id", 46 | ClientSecret: "client_secret", 47 | RedirectURL: "refresh_url", 48 | Scopes: []string{"https://www.googleapis.com/auth/drive"}, 49 | })) 50 | 51 | router := http.NewServeMux() 52 | 53 | //routes added to mux do not require authentication 54 | router.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 55 | token := oauth2.GetToken(req) 56 | if token == nil || !token.Valid() { 57 | fmt.Fprintf(w, "not logged in, or the access token is expired") 58 | return 59 | } 60 | fmt.Fprintf(w, "logged in") 61 | return 62 | }) 63 | 64 | //There is probably a nicer way to handle this than repeat the restricted routes again 65 | //of course, you could use something like gorilla/mux and define prefix / regex etc. 66 | router.Handle("/restrict", secure) 67 | 68 | n.UseHandler(router) 69 | 70 | n.Run(":3000") 71 | } 72 | ~~~ 73 | 74 | ## Auth flow 75 | 76 | * `/login` will redirect user to the OAuth 2.0 provider's permissions dialog. If there is a `next` query param provided, user is redirected to the next page afterwards. 77 | * If user agrees to connect, OAuth 2.0 provider will redirect to `/oauth2callback` to let your app to make the handshake. You need to register `/oauth2callback` as a Redirect URL in your application settings. 78 | * `/logout` will log the user out. If there is a `next` query param provided, user is redirected to the next page afterwards. 79 | 80 | You can customize the login, logout, oauth2callback and error paths: 81 | 82 | ~~~ go 83 | oauth2.PathLogin = "/oauth2login" 84 | oauth2.PathLogout = "/oauth2logout" 85 | ... 86 | ~~~ 87 | 88 | ## Contributors 89 | * [David Bochenski](http://github.com/bochenski) 90 | * [JT Olds](https://github.com/jtolds) 91 | * [minjatJ](https://github.com/minjatJ) 92 | * [Thibaut Colar](https://github.com/tcolar) 93 | * [Matt Bostock](https://github.com/mattbostock) 94 | * [Deniz Eren](https://github.com/denizeren) 95 | * [Lev Orekhov](https://github.com/lorehov) 96 | 97 | ## Derived from [martini-contrib/oauth2](http://github.com/martini-contrib/oauth2) 98 | 99 | * [Burcu Dogan](http://github.com/rakyll) 100 | -------------------------------------------------------------------------------- /oauth2_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 GoIncremental Limited. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package oauth2_test contains tests for the oauth2 package 16 | // user login via an OAuth 2.0 backend. 17 | 18 | package oauth2 19 | 20 | import ( 21 | "fmt" 22 | "net/http" 23 | "net/http/httptest" 24 | "strings" 25 | "testing" 26 | 27 | sessions "github.com/goincremental/negroni-sessions" 28 | "github.com/goincremental/negroni-sessions/cookiestore" 29 | "github.com/urfave/negroni" 30 | ) 31 | 32 | func Test_LoginRedirect(t *testing.T) { 33 | recorder := httptest.NewRecorder() 34 | n := negroni.New() 35 | n.Use(sessions.Sessions("my_session", cookiestore.New([]byte("secret123")))) 36 | n.Use(Google(&Config{ 37 | ClientID: "client_id", 38 | ClientSecret: "client_secret", 39 | RedirectURL: "refresh_url", 40 | Scopes: []string{"x", "y"}, 41 | })) 42 | 43 | r, _ := http.NewRequest("GET", "/login", nil) 44 | n.ServeHTTP(recorder, r) 45 | 46 | location := recorder.HeaderMap["Location"][0] 47 | if recorder.Code != 302 { 48 | t.Errorf("Not being redirected to the auth page.") 49 | } 50 | t.Logf(location) 51 | if strings.HasPrefix("https://accounts.google.com/o/oauth2/auth?access_type=online&approval_prompt=auto&client_id=client_id&redirect_uri=refresh_url&response_type=code&scope=x+y&state=", location) { 52 | t.Errorf("Not being redirected to the right page, %v found", location) 53 | } 54 | } 55 | 56 | func Test_LoginRedirectAfterLoginRequired(t *testing.T) { 57 | recorder := httptest.NewRecorder() 58 | n := negroni.New() 59 | n.Use(sessions.Sessions("my_session", cookiestore.New([]byte("secret123")))) 60 | n.Use(Google(&Config{ 61 | ClientID: "client_id", 62 | ClientSecret: "client_secret", 63 | RedirectURL: "refresh_url", 64 | Scopes: []string{"x", "y"}, 65 | })) 66 | 67 | n.Use(LoginRequired()) 68 | 69 | mux := http.NewServeMux() 70 | 71 | mux.HandleFunc("/login-required", func(w http.ResponseWriter, req *http.Request) { 72 | t.Log("hi there") 73 | fmt.Fprintf(w, "OK") 74 | }) 75 | 76 | n.UseHandler(mux) 77 | 78 | r, _ := http.NewRequest("GET", "/login-required?key=value", nil) 79 | n.ServeHTTP(recorder, r) 80 | 81 | location := recorder.HeaderMap["Location"][0] 82 | if recorder.Code != 302 { 83 | t.Errorf("Not being redirected to the auth page.") 84 | } 85 | if location != "/login?next=%2Flogin-required%3Fkey%3Dvalue" { 86 | t.Errorf("Not being redirected to the right page, %v found", location) 87 | } 88 | } 89 | 90 | func Test_Logout(t *testing.T) { 91 | recorder := httptest.NewRecorder() 92 | s := cookiestore.New([]byte("secret123")) 93 | 94 | n := negroni.Classic() 95 | n.Use(sessions.Sessions("my_session", s)) 96 | n.Use(Google(&Config{ 97 | ClientID: "foo", 98 | ClientSecret: "foo", 99 | RedirectURL: "foo", 100 | })) 101 | 102 | mux := http.NewServeMux() 103 | 104 | mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 105 | SetToken(req, "dummy token") 106 | fmt.Fprintf(w, "OK") 107 | }) 108 | 109 | mux.HandleFunc("/get", func(w http.ResponseWriter, req *http.Request) { 110 | tok := GetToken(req) 111 | if tok != nil { 112 | t.Errorf("User credentials are still kept in the session.") 113 | } 114 | fmt.Fprintf(w, "OK") 115 | }) 116 | 117 | n.UseHandler(mux) 118 | logout, _ := http.NewRequest("GET", "/logout", nil) 119 | index, _ := http.NewRequest("GET", "/", nil) 120 | 121 | n.ServeHTTP(httptest.NewRecorder(), index) 122 | n.ServeHTTP(recorder, logout) 123 | 124 | if recorder.Code != 302 { 125 | t.Errorf("Not being redirected to the next page.") 126 | } 127 | } 128 | 129 | func Test_LogoutOnAccessTokenExpiration(t *testing.T) { 130 | recorder := httptest.NewRecorder() 131 | s := cookiestore.New([]byte("secret123")) 132 | 133 | n := negroni.Classic() 134 | n.Use(sessions.Sessions("my_session", s)) 135 | n.Use(Google(&Config{ 136 | ClientID: "foo", 137 | ClientSecret: "foo", 138 | RedirectURL: "foo", 139 | })) 140 | 141 | mux := http.NewServeMux() 142 | mux.HandleFunc("/addtoken", func(w http.ResponseWriter, req *http.Request) { 143 | SetToken(req, "dummy token") 144 | fmt.Fprintf(w, "OK") 145 | }) 146 | 147 | mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 148 | tok := GetToken(req) 149 | if tok != nil { 150 | t.Errorf("User not logged out although access token is expired. %v\n", tok) 151 | } 152 | }) 153 | n.UseHandler(mux) 154 | addtoken, _ := http.NewRequest("GET", "/addtoken", nil) 155 | index, _ := http.NewRequest("GET", "/", nil) 156 | n.ServeHTTP(recorder, addtoken) 157 | n.ServeHTTP(recorder, index) 158 | } 159 | 160 | func Test_LoginRequired(t *testing.T) { 161 | recorder := httptest.NewRecorder() 162 | n := negroni.Classic() 163 | n.Use(sessions.Sessions("my_session", cookiestore.New([]byte("secret123")))) 164 | n.Use(Google(&Config{ 165 | ClientID: "foo", 166 | ClientSecret: "foo", 167 | RedirectURL: "foo", 168 | })) 169 | 170 | n.Use(LoginRequired()) 171 | 172 | mux := http.NewServeMux() 173 | 174 | mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 175 | fmt.Fprintf(w, "OK") 176 | }) 177 | 178 | n.UseHandler(mux) 179 | r, _ := http.NewRequest("GET", "/", nil) 180 | n.ServeHTTP(recorder, r) 181 | if recorder.Code != 302 { 182 | t.Errorf("Not being redirected to the auth page although user is not logged in. %d\n", recorder.Code) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /oauth2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 GoIncremental Limited. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package oauth2 contains Negroni middleware to provide 16 | // user login via an OAuth 2.0 backend. 17 | 18 | package oauth2 19 | 20 | import ( 21 | "crypto/rand" 22 | "encoding/hex" 23 | "encoding/json" 24 | "fmt" 25 | "net/http" 26 | "net/url" 27 | "time" 28 | 29 | sessions "github.com/goincremental/negroni-sessions" 30 | "github.com/urfave/negroni" 31 | "golang.org/x/oauth2" 32 | ) 33 | 34 | const ( 35 | codeRedirect = 302 36 | keyToken = "oauth2_token" 37 | keyNextPage = "next" 38 | keyState = "state" 39 | ) 40 | 41 | var ( 42 | // PathLogin sets the path to handle OAuth 2.0 logins. 43 | PathLogin = "/login" 44 | // PathLogout sets to handle OAuth 2.0 logouts. 45 | PathLogout = "/logout" 46 | // PathCallback sets the path to handle callback from OAuth 2.0 backend 47 | // to exchange credentials. 48 | PathCallback = "/oauth2callback" 49 | // PathError sets the path to handle error cases. 50 | PathError = "/oauth2error" 51 | ) 52 | 53 | type Config oauth2.Config 54 | 55 | // Tokens Represents a container that contains 56 | // user's OAuth 2.0 access and refresh tokens. 57 | type Tokens interface { 58 | Access() string 59 | Refresh() string 60 | Valid() bool 61 | ExpiryTime() time.Time 62 | ExtraData(string) interface{} 63 | Get() Token 64 | } 65 | 66 | type Token oauth2.Token 67 | 68 | type token struct { 69 | oauth2.Token 70 | } 71 | 72 | func (t *token) ExtraData(key string) interface{} { 73 | return t.Extra(key) 74 | } 75 | 76 | // Returns the access token. 77 | func (t *token) Access() string { 78 | return t.AccessToken 79 | } 80 | 81 | // Returns the refresh token. 82 | func (t *token) Refresh() string { 83 | return t.RefreshToken 84 | } 85 | 86 | // Returns whether the access token is 87 | // expired or not. 88 | func (t *token) Valid() bool { 89 | if t == nil { 90 | return true 91 | } 92 | return t.Token.Valid() 93 | } 94 | 95 | // Returns the expiry time of the user's 96 | // access token. 97 | func (t *token) ExpiryTime() time.Time { 98 | return t.Expiry 99 | } 100 | 101 | // String returns the string representation of the token. 102 | func (t *token) String() string { 103 | return fmt.Sprintf("tokens: %v", t) 104 | } 105 | 106 | // Returns oauth2.Token. 107 | func (t *token) Get() Token { 108 | return (Token)(t.Token) 109 | } 110 | 111 | // Returns a new Google OAuth 2.0 backend endpoint. 112 | func Google(config *Config) negroni.Handler { 113 | authUrl := "https://accounts.google.com/o/oauth2/auth" 114 | tokenUrl := "https://accounts.google.com/o/oauth2/token" 115 | return NewOAuth2Provider(config, authUrl, tokenUrl) 116 | } 117 | 118 | // Returns a new Github OAuth 2.0 backend endpoint. 119 | func Github(config *Config) negroni.Handler { 120 | authUrl := "https://github.com/login/oauth/authorize" 121 | tokenUrl := "https://github.com/login/oauth/access_token" 122 | return NewOAuth2Provider(config, authUrl, tokenUrl) 123 | } 124 | 125 | func Facebook(config *Config) negroni.Handler { 126 | authUrl := "https://www.facebook.com/dialog/oauth" 127 | tokenUrl := "https://graph.facebook.com/oauth/access_token" 128 | return NewOAuth2Provider(config, authUrl, tokenUrl) 129 | } 130 | 131 | func LinkedIn(config *Config) negroni.Handler { 132 | authUrl := "https://www.linkedin.com/uas/oauth2/authorization" 133 | tokenUrl := "https://www.linkedin.com/uas/oauth2/accessToken" 134 | return NewOAuth2Provider(config, authUrl, tokenUrl) 135 | } 136 | 137 | // Returns a generic OAuth 2.0 backend endpoint. 138 | func NewOAuth2Provider(config *Config, authUrl, tokenUrl string) negroni.HandlerFunc { 139 | c := &oauth2.Config{ 140 | ClientID: config.ClientID, 141 | ClientSecret: config.ClientSecret, 142 | Scopes: config.Scopes, 143 | RedirectURL: config.RedirectURL, 144 | Endpoint: oauth2.Endpoint{ 145 | AuthURL: authUrl, 146 | TokenURL: tokenUrl, 147 | }, 148 | } 149 | 150 | return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 151 | s := sessions.GetSession(r) 152 | 153 | if r.Method == "GET" { 154 | switch r.URL.Path { 155 | case PathLogin: 156 | login(c, s, w, r) 157 | case PathLogout: 158 | logout(s, w, r) 159 | case PathCallback: 160 | handleOAuth2Callback(c, s, w, r) 161 | default: 162 | next(w, r) 163 | } 164 | } else { 165 | next(w, r) 166 | } 167 | 168 | } 169 | } 170 | 171 | func GetToken(r *http.Request) Tokens { 172 | s := sessions.GetSession(r) 173 | t := unmarshallToken(s) 174 | 175 | //not doing this doesn't pass through the 176 | //nil return, causing a test to fail - not sure why?? 177 | if t == nil { 178 | return nil 179 | } else { 180 | return t 181 | } 182 | } 183 | 184 | func SetToken(r *http.Request, t interface{}) { 185 | s := sessions.GetSession(r) 186 | val, _ := json.Marshal(t) 187 | s.Set(keyToken, val) 188 | //Check immediately to see if the token is expired 189 | tk := unmarshallToken(s) 190 | if tk != nil { 191 | // check if the access token is expired 192 | if !tk.Valid() && tk.Refresh() == "" { 193 | s.Delete(keyToken) 194 | tk = nil 195 | } 196 | } 197 | } 198 | 199 | // Handler that redirects user to the login page 200 | // if user is not logged in. 201 | func LoginRequired() negroni.HandlerFunc { 202 | return func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 203 | token := GetToken(r) 204 | if token == nil || !token.Valid() { 205 | // Set token to null to avoid redirection loop 206 | SetToken(r, nil) 207 | next := url.QueryEscape(r.URL.RequestURI()) 208 | http.Redirect(rw, r, PathLogin+"?"+keyNextPage+"="+next, http.StatusFound) 209 | } else { 210 | next(rw, r) 211 | } 212 | } 213 | } 214 | 215 | func newState() string { 216 | var p [16]byte 217 | _, err := rand.Read(p[:]) 218 | if err != nil { 219 | panic(err) 220 | } 221 | return hex.EncodeToString(p[:]) 222 | } 223 | 224 | func login(config *oauth2.Config, s sessions.Session, w http.ResponseWriter, r *http.Request) { 225 | next := extractPath(r.URL.Query().Get(keyNextPage)) 226 | 227 | if s.Get(keyToken) == nil { 228 | // User is not logged in. 229 | if next == "" { 230 | next = "/" 231 | } 232 | 233 | state := newState() 234 | // store the next url and state token in the session 235 | s.Set(keyState, state) 236 | s.Set(keyNextPage, next) 237 | http.Redirect(w, r, config.AuthCodeURL(state, oauth2.AccessTypeOffline), http.StatusFound) 238 | return 239 | } 240 | // No need to login, redirect to the next page. 241 | http.Redirect(w, r, next, http.StatusFound) 242 | } 243 | 244 | func logout(s sessions.Session, w http.ResponseWriter, r *http.Request) { 245 | next := extractPath(r.URL.Query().Get(keyNextPage)) 246 | s.Delete(keyToken) 247 | http.Redirect(w, r, next, http.StatusFound) 248 | } 249 | 250 | func handleOAuth2Callback(config *oauth2.Config, s sessions.Session, w http.ResponseWriter, r *http.Request) { 251 | providedState := extractPath(r.URL.Query().Get("state")) 252 | 253 | //verify that the provided state is the state we generated 254 | //if it is not, then redirect to the error page 255 | originalState := s.Get(keyState) 256 | if providedState != originalState { 257 | http.Redirect(w, r, PathError, http.StatusFound) 258 | return 259 | } 260 | 261 | next := s.Get(keyNextPage).(string) 262 | code := r.URL.Query().Get("code") 263 | t, err := config.Exchange(oauth2.NoContext, code) 264 | if err != nil { 265 | // Pass the error message, or allow dev to provide its own 266 | // error handler. 267 | http.Redirect(w, r, PathError, http.StatusFound) 268 | return 269 | } 270 | // Store the credentials in the session. 271 | val, _ := json.Marshal(t) 272 | s.Set(keyToken, val) 273 | http.Redirect(w, r, next, http.StatusFound) 274 | } 275 | 276 | func unmarshallToken(s sessions.Session) *token { 277 | 278 | if s.Get(keyToken) == nil { 279 | return nil 280 | } 281 | 282 | data := s.Get(keyToken).([]byte) 283 | var tk oauth2.Token 284 | json.Unmarshal(data, &tk) 285 | return &token{tk} 286 | 287 | } 288 | 289 | func extractPath(next string) string { 290 | n, err := url.Parse(next) 291 | if err != nil { 292 | return "/" 293 | } 294 | return n.Path 295 | } 296 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. --------------------------------------------------------------------------------