// TestSecurityHeaders makes sure security headers are sent along the authorization form. func TestSecurityHeaders(t *testing.T) { cfg := setupTest() provider := test.NewProvider(true) cfg.provider = provider state := "mystate" scopes := "read write identity" grantType := "code" values := url.Values{ "client_id": {provider.Client.ID}, "state": {state}, "response_type": {grantType}, "redirect_uri": {provider.Client.RedirectURL.String()}, "scope": {scopes}, } // http://tools.ietf.org/html/rfc6749#section-4.1.1 queryStr := values.Encode() req, err := http.NewRequest("GET", "https://example.com/oauth2/authzs?"+queryStr, nil) ok(t, err) w := httptest.NewRecorder() CreateGrant(w, req, cfg) //log.Printf("%+v", w.HeaderMap) equals(t, "max-age=0", w.Header().Get("Strict-Transport-Security")) equals(t, "1; mode=block", w.Header().Get("X-XSS-Protection")) equals(t, "nosniff", w.Header().Get("X-Content-Type-Options")) equals(t, "SAMEORIGIN", w.Header().Get("X-Frame-Options")) }
// TestRedirectURIScheme makes sure clients provide redirect URLs that use TLS func TestRedirectURIScheme(t *testing.T) { cfg := setupTest() provider := test.NewProvider(true) cfg.provider = provider state := "state-test" scopes := "read write identity" grantType := "code" values := url.Values{ "client_id": {provider.Client.ID}, "response_type": {grantType}, "state": {state}, "redirect_uri": {"http://attacker.com/callback"}, "scope": {scopes}, } // http://tools.ietf.org/html/rfc6749#section-4.1.1 queryStr := values.Encode() req, err := http.NewRequest("GET", "https://example.com/oauth2/authzs?"+queryStr, nil) ok(t, err) w := httptest.NewRecorder() CreateGrant(w, req, cfg) body := w.Body.String() assert(t, strings.Contains(body, "access_denied") == true, "access-denied was not found in response body") assert(t, strings.Contains(body, "3rd-party client app provided an invalid redirect_uri. It does not comply with http://tools.ietf.org/html/rfc3986#section-4.3 or does not use HTTPS") == true, "error description does not match.") }
// TestResourceOwnerCredentialsGrant tests happy path for http://tools.ietf.org/html/rfc6749#section-4.3 func TestResourceOwnerCredentialsGrant(t *testing.T) { cfg := setupTest() cfg.provider = test.NewProvider(true) queryStr := url.Values{ "grant_type": {"password"}, "username": {"test_user"}, "password": {"test_password"}, } buffer := bytes.NewBufferString(queryStr.Encode()) req, err := http.NewRequest("POST", "https://example.com/oauth2/tokens", buffer) ok(t, err) req.Header.Set("Content-type", "application/x-www-form-urlencoded") req.SetBasicAuth("testclient", "testclient") w := httptest.NewRecorder() IssueToken(w, req, cfg) accessToken := types.Token{} err = json.Unmarshal(w.Body.Bytes(), &accessToken) ok(t, err) //log.Printf("%s", w.Body.String()) equals(t, "bearer", accessToken.Type) equals(t, "600", accessToken.ExpiresIn) assert(t, accessToken.RefreshToken != "", "we were expecting a refresh token.") // Tests that cache headers are being sent when generating tokens using // resource owner credentials. equals(t, "no-store", w.Header().Get("Cache-Control")) equals(t, "no-cache", w.Header().Get("Pragma")) equals(t, "0", w.Header().Get("Expires")) }
// TestStateIsRequired makes sure it requires clients to provide a state when // getting authorization codes. func TestStateIsRequired(t *testing.T) { cfg := setupTest() provider := test.NewProvider(true) cfg.provider = provider scopes := "read write identity" grantType := "code" values := url.Values{ "client_id": {provider.Client.ID}, "response_type": {grantType}, "redirect_uri": {provider.Client.RedirectURL.String()}, "scope": {scopes}, } // http://tools.ietf.org/html/rfc6749#section-4.1.1 queryStr := values.Encode() req, err := http.NewRequest("GET", "https://example.com/oauth2/authzs?"+queryStr, nil) ok(t, err) w := httptest.NewRecorder() CreateGrant(w, req, cfg) equals(t, http.StatusFound, w.Code) u, err := url.Parse(w.Header().Get("Location")) ok(t, err) equals(t, "invalid_request", u.Query().Get("error")) equals(t, "state parameter is required by this authorization server.", u.Query().Get("error_description")) }
// TestRefreshToken tests happy path for http://tools.ietf.org/html/rfc6749#section-6 func TestRefreshToken(t *testing.T) { cfg := setupTest() provider := test.NewProvider(true) cfg.provider = provider noAuthzGrant := types.Grant{ Scopes: types.Scopes{ types.Scope{ID: "identity"}, }, } accessToken, err := provider.GenToken(noAuthzGrant, types.Client{ ID: "test_client_id", }, true, cfg.tokenExpiration) ok(t, err) queryStr := url.Values{ "grant_type": {"refresh_token"}, "refresh_token": {accessToken.RefreshToken}, "scope": {"identity"}, } buffer := bytes.NewBufferString(queryStr.Encode()) req, err := http.NewRequest("POST", "https://example.com/oauth2/tokens", buffer) ok(t, err) req.Header.Set("Content-type", "application/x-www-form-urlencoded") req.SetBasicAuth("testclient", "testclient") w := httptest.NewRecorder() IssueToken(w, req, cfg) token := types.Token{} err = json.Unmarshal(w.Body.Bytes(), &token) ok(t, err) //log.Printf("%s", w.Body.String()) equals(t, "bearer", token.Type) equals(t, "600", token.ExpiresIn) assert(t, accessToken.Value != token.Value, "We got the same access token, it should be different!") assert(t, token.Value != "", "We were expecting to get a token and instead we got: %s", token.Value) assert(t, token.RefreshToken != "", "we were expecting a refresh token.") assert(t, token.RefreshToken != accessToken.RefreshToken, "We got the same refresh token, it should be different!") // Tests that cache headers are being sent when refreshing tokens equals(t, "no-store", w.Header().Get("Cache-Control")) equals(t, "no-cache", w.Header().Get("Pragma")) equals(t, "0", w.Header().Get("Expires")) }
// TestRedirectURLMatch makes sure redirect_uri for requesting an authorization // grant is the same as the redirect_uri provided to get the correspondent access token. // This is intended to mitigate the risk of account hijacking by leaking // authorization codes. func TestRedirectURLMatch(t *testing.T) { cfg := setupTest() provider := test.NewProvider(true) cfg.provider = provider state := "state-test" scopes := "read write identity" grantType := "code" values := url.Values{ "client_id": {provider.Client.ID}, "response_type": {grantType}, "state": {state}, "redirect_uri": {provider.Client.RedirectURL.String()}, "scope": {scopes}, } // http://tools.ietf.org/html/rfc6749#section-4.1.1 queryStr := values.Encode() req, err := http.NewRequest("GET", "https://example.com/oauth2/authzs?"+queryStr, nil) ok(t, err) w := httptest.NewRecorder() CreateGrant(w, req, cfg) equals(t, http.StatusOK, w.Code) // Sending post to acquire authorization token values.Set("redirect_uri", "https://attacker.com/callback") queryStr2 := values.Encode() buffer := bytes.NewBufferString(queryStr2) req, err = http.NewRequest("POST", "https://example.com/oauth2/authzs", buffer) ok(t, err) req.Header.Set("Content-type", "application/x-www-form-urlencoded") w2 := httptest.NewRecorder() CreateGrant(w2, req, cfg) body := w2.Body.String() assert(t, strings.Contains(body, "access_denied"), "access_denied was expected as response") assert(t, strings.Contains(body, "3rd-party client app provided a redirect_uri that does not match the URI registered for this client in our database."), "unexpected error description.") }
// TestLoginRedirect tests that logging in is required for a resource owner to // grant any authorization codes to clients. func TestLoginRedirect(t *testing.T) { cfg := setupTest() provider := test.NewProvider(false) cfg.provider = provider state := "state-test" scopes := "read write identity" grantType := "code" clientID := provider.Client.ID redirectURL := provider.Client.RedirectURL.String() values := url.Values{ "client_id": {clientID}, "response_type": {grantType}, "state": {state}, "redirect_uri": {redirectURL}, "scope": {scopes}, } // http://tools.ietf.org/html/rfc6749#section-4.1.1 queryStr := values.Encode() authzURL := "https://example.com/oauth2/authzs?" + queryStr req, err := http.NewRequest("GET", authzURL, nil) ok(t, err) w := httptest.NewRecorder() CreateGrant(w, req, cfg) equals(t, http.StatusFound, w.Code) loginURL := cfg.loginURL.url query := loginURL.Query() query.Set(cfg.loginURL.redirectParam, authzURL) loginURL.RawQuery = query.Encode() equals(t, loginURL.String(), w.Header().Get("Location")) }
// getTestAuthzCode returns authorization tokens for access tokens issuing tests func getTestAuthzCode(t *testing.T) (config, string) { cfg := setupTest() provider := test.NewProvider(true) cfg.provider = provider state := "state-test" scopes := "read write identity" grantType := "code" values := url.Values{ "client_id": {provider.Client.ID}, "response_type": {grantType}, "state": {state}, "redirect_uri": {provider.Client.RedirectURL.String()}, "scope": {scopes}, } // http://tools.ietf.org/html/rfc6749#section-4.1.1 queryStr := values.Encode() req, err := http.NewRequest("GET", "https://example.com/oauth2/authzs?"+queryStr, nil) ok(t, err) w := httptest.NewRecorder() CreateGrant(w, req, cfg) equals(t, http.StatusOK, w.Code) body := w.Body.String() stringz := []string{ "client_id", "redirect_uri", "response_type", "state", "scope", "code", "read write identity", "state-test", } for _, s := range stringz { assert(t, strings.Contains(body, s), "Does not look like we got an authorization form: '%s' was not found in %v", s, body) } // Sending post to acquire authorization token buffer := bytes.NewBufferString(queryStr) req, err = http.NewRequest("POST", "https://example.com/oauth2/authzs", buffer) ok(t, err) req.Header.Set("Content-type", "application/x-www-form-urlencoded") w = httptest.NewRecorder() CreateGrant(w, req, cfg) // Tests http://tools.ietf.org/html/rfc6749#section-4.1.2 equals(t, http.StatusFound, w.Code) redirectTo := w.Header().Get("Location") u, err := url.Parse(redirectTo) ok(t, err) authzCode := u.Query().Get("code") assert(t, authzCode != "", "It looks like the authorization code came back empty: %s", authzCode) // makes sure the same state parameter value received to acquire // the authorization grant is send back when delivering the access token. equals(t, state, u.Query().Get("state")) return cfg, authzCode }
// TestImplicitGrant tests a happy implicit flow func TestImplicitGrant(t *testing.T) { cfg := setupTest() provider := test.NewProvider(true) cfg.provider = provider state := "state-test" scopes := "read write identity" grantType := "token" clientID := provider.Client.ID redirectURL := provider.Client.RedirectURL.String() values := url.Values{ "client_id": {clientID}, "response_type": {grantType}, "state": {state}, "redirect_uri": {redirectURL}, "scope": {scopes}, } // http://tools.ietf.org/html/rfc6749#section-4.2.1 queryStr := values.Encode() authzURL := "https://example.com/oauth2/authzs?" + queryStr req, err := http.NewRequest("GET", authzURL, nil) ok(t, err) w := httptest.NewRecorder() CreateGrant(w, req, cfg) body := w.Body.String() stringz := []string{ "client_id", "redirect_uri", "response_type", "state", "scope", "token", "read write identity", "state-test", } for _, s := range stringz { assert(t, strings.Contains(body, s), "Does not look like we got an authorization form: '%s' was not found in %v", s, body) } // Sending post to acquire authorization token buffer := bytes.NewBufferString(queryStr) req, err = http.NewRequest("POST", "https://example.com/oauth2/authzs", buffer) ok(t, err) req.Header.Set("Content-type", "application/x-www-form-urlencoded") w = httptest.NewRecorder() CreateGrant(w, req, cfg) // Tests http://tools.ietf.org/html/rfc6749#section-4.2.2 equals(t, http.StatusFound, w.Code) redirectTo := w.Header().Get("Location") u, err := url.Parse(redirectTo) ok(t, err) fragment, err := url.ParseQuery(strings.TrimPrefix(u.Fragment, "#")) ok(t, err) accessToken := fragment.Get("access_token") assert(t, accessToken != "", "It looks like the authorization code came back empty: ->%s<-", accessToken) equals(t, state, fragment.Get("state")) equals(t, "600", fragment.Get("expires_in")) equals(t, scopes, fragment.Get("scope")) equals(t, "bearer", fragment.Get("token_type")) // Implict flow should not emit refresh tokens refreshToken := fragment.Get("refresh_token") equals(t, "", refreshToken) }
func ExampleExamples_basic() { // Authorization form authzForm := ` <html> <body> {{if .Errors}} <div id="errors"> <ul> {{range .Errors}} <li>{{.Code}}: {{.Desc}}</li> {{end}} </ul> </div> {{else}} <div id="client"> <h2>{{.Client.Name}}</h2> <h3>{{.Client.Desc}}</h3> <a href="{{.Client.HomepageURL}}"> <figure><img src="{{.Client.ProfileImgURL}}"/></figure> </a> </div> <div id="scopes"> <ul> {{range .Scopes}} <li>{{.ID}}: {{.Desc}}</li> {{end}} </ul> </div> <form> <input type="hidden" name="client_id" value="{{.Client.ID}}"/> <input type="hidden" name="response_type" value="{{.GrantType}}"/> <input type="hidden" name="redirect_uri" value="{{.Client.RedirectURL}}"/> <input type="hidden" name="scope" value="{{.Scopes.Encode}}"/> <input type="hidden" name="state" value="{{.State}}"/> </form> {{end}} </body> </html> ` mux := http.NewServeMux() mux.HandleFunc("/hello", func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Hellow World!")) }) provider := test.NewProvider(true) // Authorization handler to protect resources in this server authzHandler := AuthzHandler(mux, provider) // OAuth2 handler oauth2Handlers := Handler(authzHandler, SetProvider(provider), SetAuthzForm(authzForm), SetAuthzEndpoint("/oauth2/authorize"), SetTokenEndpoint("/oauth2/tokens"), SetSTSMaxAge(time.Duration(8760)*time.Hour), // 1yr SetAuthzExpiration(time.Duration(1)*time.Minute), SetTokenExpiration(time.Duration(10)*time.Minute), SetLoginURL("https://api.hooklift.io/accounts/login", "redirect_to"), ) log.Fatal(http.ListenAndServe(":3000", oauth2Handlers)) }