diff --git a/api/pkg/auth/service/auth.go b/api/pkg/auth/service/auth.go index 4280975eb4..1a80764209 100644 --- a/api/pkg/auth/service/auth.go +++ b/api/pkg/auth/service/auth.go @@ -62,6 +62,20 @@ type Service interface { HubAuthenticate(res http.ResponseWriter, req *http.Request) } +type cookie struct { + Name string + Value string + MaxAge int + Path string + HttpOnly bool +} + +const ( + RefreshToken = "refreshToken" + AccessToken = "accessToken" + Path = "/" +) + // New returns the auth service implementation. func New(api app.Config) Service { return &service{ @@ -192,6 +206,27 @@ func (s *service) HubAuthenticate(res http.ResponseWriter, req *http.Request) { return } + //Add cookie and response and send it to in header + + refreshToken := cookie{ + Name: RefreshToken, + Value: userTokens.Data.Refresh.Token, + MaxAge: int(r.jwtConfig.RefreshExpiresIn.Seconds()), + Path: Path, + HttpOnly: true, + } + + accessToken := cookie{ + Name: AccessToken, + Value: userTokens.Data.Access.Token, + MaxAge: int(r.jwtConfig.AccessExpiresIn.Seconds()), + Path: Path, + HttpOnly: true, + } + + refreshToken.createCookie(res) + accessToken.createCookie(res) + res.WriteHeader(http.StatusOK) if err := json.NewEncoder(res).Encode(userTokens); err != nil { r.log.Error(err) @@ -229,3 +264,15 @@ func List(res http.ResponseWriter, req *http.Request) { return } } + +func (c cookie) createCookie(res http.ResponseWriter) { + + cookie := &http.Cookie{ + Name: c.Name, + Value: c.Value, + MaxAge: c.MaxAge, + Path: c.Path, + HttpOnly: c.HttpOnly, + } + http.SetCookie(res, cookie) +} diff --git a/api/pkg/service/rating/rating.go b/api/pkg/service/rating/rating.go index 68514803da..5ae041da43 100644 --- a/api/pkg/service/rating/rating.go +++ b/api/pkg/service/rating/rating.go @@ -23,6 +23,7 @@ import ( "github.com/tektoncd/hub/api/pkg/app" "github.com/tektoncd/hub/api/pkg/db/model" "github.com/tektoncd/hub/api/pkg/service/validator" + "goa.design/goa/v3/security" "gorm.io/gorm" ) @@ -50,10 +51,21 @@ func New(api app.Config) rating.Service { // Find user's rating for a resource func (s *service) Get(ctx context.Context, p *rating.GetPayload) (*rating.GetResult, error) { + // Required scope to get the user rating for a resource + requiredScope := &security.JWTScheme{ + RequiredScopes: []string{"rating:read"}, + } + + //Verify and validate the required scopes + newctx, err := s.JWTAuth(ctx, p.Session, requiredScope) + if err != nil { + return &rating.GetResult{Rating: 0}, err + } + req := request{ - db: s.DB(ctx), - log: s.Logger(ctx), - userID: validator.UserID(ctx), + db: s.DB(newctx), + log: s.Logger(newctx), + userID: validator.UserID(newctx), } return req.getRating(p.ID) @@ -62,10 +74,22 @@ func (s *service) Get(ctx context.Context, p *rating.GetPayload) (*rating.GetRes // Update user's rating for a resource func (s *service) Update(ctx context.Context, p *rating.UpdatePayload) error { + // Required scope to get the user rating for a resource + requiredScope := &security.JWTScheme{ + RequiredScopes: []string{"rating:write"}, + } + + //Verify and validate the required scopes + newctx, err := s.JWTAuth(ctx, p.Session, requiredScope) + + if err != nil { + return err + } + req := request{ - db: s.DB(ctx), - log: s.Logger(ctx), - userID: validator.UserID(ctx), + db: s.DB(newctx), + log: s.Logger(newctx), + userID: validator.UserID(newctx), } return req.updateRating(p.ID, p.Rating) diff --git a/api/pkg/service/rating/rating_http_test.go b/api/pkg/service/rating/rating_http_test.go index 9ed3a6a518..4d7026d3f5 100644 --- a/api/pkg/service/rating/rating_http_test.go +++ b/api/pkg/service/rating/rating_http_test.go @@ -26,17 +26,16 @@ import ( "github.com/tektoncd/hub/api/gen/http/rating/server" "github.com/tektoncd/hub/api/gen/rating" "github.com/tektoncd/hub/api/pkg/db/model" - "github.com/tektoncd/hub/api/pkg/service/validator" "github.com/tektoncd/hub/api/pkg/testutils" goa "goa.design/goa/v3/pkg" ) func GetChecker(tc *testutils.TestConfig) *goahttpcheck.APIChecker { - service := validator.NewService(tc.APIConfig, "rating") + // service := validator.NewService(tc.APIConfig, "rating") checker := goahttpcheck.New() checker.Mount(server.NewGetHandler, server.MountGetHandler, - rating.NewGetEndpoint(New(tc), service.JWTAuth)) + rating.NewGetEndpoint(New(tc))) return checker } @@ -45,7 +44,7 @@ func TestGet_Http_InvalidToken(t *testing.T) { testutils.LoadFixtures(t, tc.FixturePath()) GetChecker(tc).Test(t, http.MethodGet, "/resource/1/rating"). - WithHeader("Authorization", "invalidToken").Check(). + WithCookie("accessToken", "invalidToken").Check(). HasStatus(401).Cb(func(r *http.Response) { b, readErr := ioutil.ReadAll(r.Body) assert.NoError(t, readErr) @@ -73,7 +72,7 @@ func TestGet_Http_ExpiredToken(t *testing.T) { jwt.TimeFunc = testutils.NowAfterDuration(tc.JWTConfig().AccessExpiresIn) GetChecker(tc).Test(t, http.MethodGet, "/resource/1/rating"). - WithHeader("Authorization", accessToken).Check(). + WithCookie("accessToken", accessToken).Check(). HasStatus(401).Cb(func(r *http.Response) { b, readErr := ioutil.ReadAll(r.Body) assert.NoError(t, readErr) @@ -100,7 +99,7 @@ func TestGet_Http_InvalidScopes(t *testing.T) { jwt.TimeFunc = testutils.Now GetChecker(tc).Test(t, http.MethodGet, "/resource/1/rating"). - WithHeader("Authorization", accessToken).Check(). + WithCookie("accessToken", accessToken).Check(). HasStatus(403).Cb(func(r *http.Response) { b, readErr := ioutil.ReadAll(r.Body) assert.NoError(t, readErr) @@ -127,7 +126,7 @@ func TestGet_Http(t *testing.T) { jwt.TimeFunc = testutils.Now GetChecker(tc).Test(t, http.MethodGet, "/resource/1/rating"). - WithHeader("Authorization", accessToken).Check(). + WithCookie("accessToken", accessToken).Check(). HasStatus(200).Cb(func(r *http.Response) { b, readErr := ioutil.ReadAll(r.Body) assert.NoError(t, readErr) @@ -154,7 +153,7 @@ func TestGet_Http_RatingNotFound(t *testing.T) { jwt.TimeFunc = testutils.Now GetChecker(tc).Test(t, http.MethodGet, "/resource/3/rating"). - WithHeader("Authorization", accessToken).Check(). + WithCookie("accessToken", accessToken).Check(). HasStatus(200).Cb(func(r *http.Response) { b, readErr := ioutil.ReadAll(r.Body) assert.NoError(t, readErr) @@ -181,7 +180,7 @@ func TestGet_Http_ResourceNotFound(t *testing.T) { jwt.TimeFunc = testutils.Now GetChecker(tc).Test(t, http.MethodGet, "/resource/99/rating"). - WithHeader("Authorization", accessToken).Check(). + WithCookie("accessToken", accessToken).Check(). HasStatus(404).Cb(func(r *http.Response) { b, readErr := ioutil.ReadAll(r.Body) assert.NoError(t, readErr) @@ -195,11 +194,11 @@ func TestGet_Http_ResourceNotFound(t *testing.T) { } func UpdateChecker(tc *testutils.TestConfig) *goahttpcheck.APIChecker { - service := validator.NewService(tc.APIConfig, "rating") + // service := validator.NewService(tc.APIConfig, "rating") checker := goahttpcheck.New() checker.Mount(server.NewUpdateHandler, server.MountUpdateHandler, - rating.NewUpdateEndpoint(New(tc), service.JWTAuth)) + rating.NewUpdateEndpoint(New(tc))) return checker } @@ -218,7 +217,7 @@ func TestUpdate_Http(t *testing.T) { data := []byte(`{"rating": 5}`) UpdateChecker(tc).Test(t, http.MethodPut, "/resource/3/rating"). - WithHeader("Authorization", accessToken). + WithCookie("accessToken", accessToken). WithBody(data).Check(). HasStatus(200) @@ -244,7 +243,7 @@ func TestUpdate_Http_Existing(t *testing.T) { data := []byte(`{"rating": 2}`) UpdateChecker(tc).Test(t, http.MethodPut, "/resource/1/rating"). - WithHeader("Authorization", accessToken). + WithCookie("accessToken", accessToken). WithBody(data).Check(). HasStatus(200) @@ -270,7 +269,7 @@ func TestUpdate_Http_ResourceNotFound(t *testing.T) { data := []byte(`{"rating": 2}`) UpdateChecker(tc).Test(t, http.MethodPut, "/resource/99/rating"). - WithHeader("Authorization", accessToken). + WithCookie("accessToken", accessToken). WithBody(data).Check(). HasStatus(404).Cb(func(r *http.Response) { b, readErr := ioutil.ReadAll(r.Body) diff --git a/api/pkg/service/rating/rating_test.go b/api/pkg/service/rating/rating_test.go index dfd5b7186d..e240039664 100644 --- a/api/pkg/service/rating/rating_test.go +++ b/api/pkg/service/rating/rating_test.go @@ -18,6 +18,7 @@ import ( "context" "testing" + "github.com/golang-jwt/jwt" "github.com/stretchr/testify/assert" "github.com/tektoncd/hub/api/gen/rating" "github.com/tektoncd/hub/api/pkg/service/validator" @@ -29,13 +30,18 @@ func TestGet(t *testing.T) { testutils.LoadFixtures(t, tc.FixturePath()) // user with rating:read scope - user, _, err := tc.UserWithScopes("foo", "foo@bar.com", "rating:read") + user, accessToken, err := tc.UserWithScopes("foo", "foo@bar.com", "rating:read") + assert.Equal(t, user.Email, "foo@bar.com") assert.NoError(t, err) + // Mocks the time + jwt.TimeFunc = testutils.Now + ratingSvc := New(tc) + ctx := validator.WithUserID(context.Background(), user.ID) - payload := &rating.GetPayload{ID: 1} + payload := &rating.GetPayload{ID: 1, Session: accessToken} rat, err := ratingSvc.Get(ctx, payload) assert.NoError(t, err) assert.Equal(t, 5, rat.Rating) @@ -46,13 +52,16 @@ func TestGet_RatingNotFound(t *testing.T) { testutils.LoadFixtures(t, tc.FixturePath()) // user with rating:read scope - user, _, err := tc.UserWithScopes("foo", "foo@bar.com", "rating:read") + user, accessToken, err := tc.UserWithScopes("foo", "foo@bar.com", "rating:read") assert.Equal(t, user.Email, "foo@bar.com") assert.NoError(t, err) + // Mocks the time + jwt.TimeFunc = testutils.Now + ratingSvc := New(tc) ctx := validator.WithUserID(context.Background(), user.ID) - payload := &rating.GetPayload{ID: 3} + payload := &rating.GetPayload{ID: 3, Session: accessToken} rat, err := ratingSvc.Get(ctx, payload) assert.NoError(t, err) assert.Equal(t, -1, rat.Rating) @@ -63,13 +72,16 @@ func TestGet_ResourceNotFound(t *testing.T) { testutils.LoadFixtures(t, tc.FixturePath()) // user with rating:read scope - user, _, err := tc.UserWithScopes("foo", "foo@bar.com", "rating:read") + user, accessToken, err := tc.UserWithScopes("foo", "foo@bar.com", "rating:read") assert.Equal(t, user.Email, "foo@bar.com") assert.NoError(t, err) + // Mocks the time + jwt.TimeFunc = testutils.Now + ratingSvc := New(tc) ctx := validator.WithUserID(context.Background(), user.ID) - payload := &rating.GetPayload{ID: 99} + payload := &rating.GetPayload{ID: 99, Session: accessToken} _, err = ratingSvc.Get(ctx, payload) assert.Error(t, err) assert.EqualError(t, err, "resource not found") @@ -80,13 +92,16 @@ func TestUpdate(t *testing.T) { testutils.LoadFixtures(t, tc.FixturePath()) // user with rating:write scope - user, _, err := tc.UserWithScopes("foo", "foo@bar.com", "rating:write") + user, accessToken, err := tc.UserWithScopes("foo", "foo@bar.com", "rating:write") assert.Equal(t, user.Email, "foo@bar.com") assert.NoError(t, err) + // Mocks the time + jwt.TimeFunc = testutils.Now + ratingSvc := New(tc) ctx := validator.WithUserID(context.Background(), user.ID) - payload := &rating.UpdatePayload{ID: 1, Rating: 3} + payload := &rating.UpdatePayload{ID: 1, Rating: 3, Session: accessToken} err = ratingSvc.Update(ctx, payload) assert.NoError(t, err) } @@ -96,13 +111,16 @@ func TestUpdate_ResourceNotFound(t *testing.T) { testutils.LoadFixtures(t, tc.FixturePath()) // user with rating:write scope - user, _, err := tc.UserWithScopes("foo", "foo@bar.com", "rating:write") + user, accessToken, err := tc.UserWithScopes("foo", "foo@bar.com", "rating:write") assert.Equal(t, user.Email, "foo@bar.com") assert.NoError(t, err) + // Mocks the time + jwt.TimeFunc = testutils.Now + ratingSvc := New(tc) ctx := validator.WithUserID(context.Background(), user.ID) - payload := &rating.UpdatePayload{ID: 99, Rating: 3} + payload := &rating.UpdatePayload{ID: 99, Rating: 3, Session: accessToken} err = ratingSvc.Update(ctx, payload) assert.Error(t, err) assert.EqualError(t, err, "resource not found") diff --git a/api/pkg/testutils/utils.go b/api/pkg/testutils/utils.go index cb9d4cc295..df315630b9 100644 --- a/api/pkg/testutils/utils.go +++ b/api/pkg/testutils/utils.go @@ -17,6 +17,7 @@ package testutils import ( "bytes" "encoding/json" + "net/http" "time" "github.com/tektoncd/hub/api/pkg/db/model" @@ -76,6 +77,16 @@ func (tc *TestConfig) UserWithScopes(name, email string, scopes ...string) (*mod return user, accessToken, nil } +// It returns a cookie object +func (tc *TestConfig) CreateCookie(name, value string) *http.Cookie { + return &http.Cookie{ + Name: name, + Value: value, + MaxAge: 300, + Path: "/", + } +} + // RefreshTokenForUser returns refresh JWT for user with refresh:token scope // User will have same github login and github name in db func (tc *TestConfig) RefreshTokenForUser(name, email string) (*model.User, string, error) { @@ -143,3 +154,25 @@ func (tc *TestConfig) AddScopesForUser(userID uint, scopes []string) error { } return nil } + +// AccessTokenForUser returns access JWT for user with access:token scope +// User will have same github login and github name in db +func (tc *TestConfig) AccessTokenForUser(name, email string) (*model.User, string, error) { + + user := &model.User{Type: model.NormalUserType, Email: email} + if err := tc.DB().Where(&model.User{Email: email}). + FirstOrCreate(user).Error; err != nil { + return nil, "", err + } + + token.Now = Now + + req := token.Request{User: user, JWTConfig: tc.JWTConfig(), Provider: "github"} + accessToken, _, err := req.AccessJWT() + + if err != nil { + return nil, "", err + } + + return user, accessToken, nil +} diff --git a/api/pkg/user/app/app.go b/api/pkg/user/app/app.go index 9e3a13df84..279b8e7840 100644 --- a/api/pkg/user/app/app.go +++ b/api/pkg/user/app/app.go @@ -64,3 +64,11 @@ type RefreshToken struct { // Refresh Token for user Refresh *Token `json:"refresh"` } + +type ClearCookies struct { + Data bool +} + +type ExitingAccessToken struct { + Data string `json:"token"` +} diff --git a/api/pkg/user/base.go b/api/pkg/user/base.go index 34ab4d2939..4a3ea8b5fe 100644 --- a/api/pkg/user/base.go +++ b/api/pkg/user/base.go @@ -30,6 +30,8 @@ func User(r *mux.Router, api app.Config) { } s.HandleFunc("/info", jwt.JWTAuth(userSvc.Info)) + s.HandleFunc("/logout", userSvc.Logout) s.HandleFunc("/refresh/accesstoken", jwt.JWTAuth(userSvc.RefreshAccessToken)) s.HandleFunc("/refresh/refreshtoken", jwt.JWTAuth(userSvc.NewRefreshToken)) + s.HandleFunc("/accesstoken", jwt.JWTAuth(userSvc.GetAccessToken)) } diff --git a/api/pkg/user/service/service.go b/api/pkg/user/service/service.go index 22445e7e87..de2d86ff29 100644 --- a/api/pkg/user/service/service.go +++ b/api/pkg/user/service/service.go @@ -43,10 +43,33 @@ type JWTScheme struct { func (s *UserService) JWTAuth(handler http.HandlerFunc) http.HandlerFunc { return func(res http.ResponseWriter, req *http.Request) { - jwt := req.Header.Get("Authorization") + accessCookie, err1 := req.Cookie("accessToken") + refreshCookie, err2 := req.Cookie("refreshToken") - if len(jwt) > 6 && strings.ToUpper(jwt[0:7]) == "BEARER " { - jwt = jwt[7:] + if err1 == http.ErrNoCookie && err2 == http.ErrNoCookie { + http.Error(res, err1.Error(), http.StatusUnauthorized) + return + } + + var defalutJWT string + if accessCookie == nil { + defalutJWT = refreshCookie.Value + } else { + defalutJWT = accessCookie.Value + } + + var jwt string + switch req.RequestURI { + case "/user/info": + jwt = accessCookie.Value + case "/user/refresh/accesstoken": + jwt = refreshCookie.Value + case "/user/refresh/refreshtoken": + jwt = refreshCookie.Value + case "/user/accesstoken", "/user/logout": + jwt = accessCookie.Value + default: + jwt = defalutJWT } claims, err := token.Verify(jwt, s.JwtConfig.SigningKey) @@ -62,7 +85,7 @@ func (s *UserService) JWTAuth(handler http.HandlerFunc) http.HandlerFunc { if req.RequestURI == "/user/info" { scheme.RequiredScopes = []string{"rating:read", "rating:write"} - } else if req.RequestURI == "/refresh/accesstoken" || req.RequestURI == "/refresh/refreshtoken" { + } else if req.RequestURI == "/refresh/accesstoken" || req.RequestURI == "/user/logout" || req.RequestURI == "/refresh/refreshtoken" { scheme.RequiredScopes = []string{"rating:read", "rating:write", "refresh:token"} } @@ -201,10 +224,6 @@ func (s *UserService) validateRefreshToken(id int, token string) (*model.User, e return nil, err } - if len(token) > 6 && strings.ToUpper(token[0:7]) == "BEARER " { - token = token[7:] - } - if user.RefreshTokenChecksum != createChecksum(token) { return nil, invalidRefreshToken } diff --git a/api/pkg/user/service/user.go b/api/pkg/user/service/user.go index dd121d47ef..729cf7c873 100644 --- a/api/pkg/user/service/user.go +++ b/api/pkg/user/service/user.go @@ -23,6 +23,7 @@ import ( "github.com/tektoncd/hub/api/gen/log" "github.com/tektoncd/hub/api/pkg/app" + auth "github.com/tektoncd/hub/api/pkg/auth/service" "github.com/tektoncd/hub/api/pkg/db/model" "github.com/tektoncd/hub/api/pkg/token" userApp "github.com/tektoncd/hub/api/pkg/user/app" @@ -48,6 +49,8 @@ type Service interface { Info(res http.ResponseWriter, req *http.Request) RefreshAccessToken(res http.ResponseWriter, req *http.Request) NewRefreshToken(res http.ResponseWriter, req *http.Request) + GetAccessToken(res http.ResponseWriter, req *http.Request) + Logout(res http.ResponseWriter, req *http.Request) } var ( @@ -64,7 +67,6 @@ func New(api app.Config) Service { } } -// Get the user Info func (s *UserService) Info(res http.ResponseWriter, req *http.Request) { id := req.Header.Get("UserID") @@ -128,7 +130,12 @@ func (s *UserService) RefreshAccessToken(res http.ResponseWriter, req *http.Requ return } - refreshToken := req.Header.Get("Authorization") + cookie, err := req.Cookie(auth.RefreshToken) + if err != nil { + http.Error(res, err.Error(), http.StatusUnauthorized) + } + refreshToken := cookie.Value + user, err := s.validateRefreshToken(userId, refreshToken) if err != nil { r.log.Error(err) @@ -136,7 +143,7 @@ func (s *UserService) RefreshAccessToken(res http.ResponseWriter, req *http.Requ return } - result, err := s.newRequest(user, provider).refreshAccessToken() + result, err := s.newRequest(user, provider).refreshAccessToken(res, req) if err != nil { http.Error(res, err.Error(), http.StatusInternalServerError) return @@ -150,26 +157,34 @@ func (s *UserService) RefreshAccessToken(res http.ResponseWriter, req *http.Requ } -func (r *request) refreshAccessToken() (*userApp.RefreshAccessTokenResult, error) { +func (r *request) refreshAccessToken(res http.ResponseWriter, req *http.Request) (*userApp.RefreshAccessTokenResult, error) { scopes, err := r.userScopes() if err != nil { return nil, err } - req := token.Request{ + request := token.Request{ User: r.user, Scopes: scopes, JWTConfig: r.jwtConfig, Provider: r.provider, } - accessToken, accessExpiresAt, err := req.AccessJWT() + accessToken, accessExpiresAt, err := request.AccessJWT() if err != nil { r.log.Error(err) return nil, refreshError } + http.SetCookie(res, &http.Cookie{ + Name: auth.AccessToken, + Value: accessToken, + MaxAge: int(r.jwtConfig.AccessExpiresIn.Seconds()), + Path: "/", + HttpOnly: true, + }) + data := &userApp.AccessToken{ Access: &userApp.Token{ Token: accessToken, @@ -185,6 +200,13 @@ func (s *UserService) NewRefreshToken(res http.ResponseWriter, req *http.Request id := req.Header.Get("UserID") provider := req.Header.Get("Provider") + cookie, err := req.Cookie("refreshToken") + if err != nil { + http.Error(res, err.Error(), http.StatusUnauthorized) + } + + refreshToken := cookie.Value + r := request{ db: s.DB(context.Background()), log: s.Logger(context.Background()), @@ -200,7 +222,6 @@ func (s *UserService) NewRefreshToken(res http.ResponseWriter, req *http.Request return } - refreshToken := req.Header.Get("Authorization") user, err := s.validateRefreshToken(userId, refreshToken) if err != nil { r.log.Error(err) @@ -208,7 +229,8 @@ func (s *UserService) NewRefreshToken(res http.ResponseWriter, req *http.Request return } - result, err := s.newRequest(user, provider).newRefreshToken() + // result, err := s.newRequest(user, provider).newRefreshToken() + result, err := s.newRequest(user, provider).newRefreshToken(res, req) if err != nil { http.Error(res, err.Error(), http.StatusInternalServerError) return @@ -222,15 +244,15 @@ func (s *UserService) NewRefreshToken(res http.ResponseWriter, req *http.Request } -func (r *request) newRefreshToken() (*userApp.NewRefreshTokenResult, error) { +func (r *request) newRefreshToken(res http.ResponseWriter, req *http.Request) (*userApp.NewRefreshTokenResult, error) { - req := token.Request{ + request := token.Request{ User: r.user, JWTConfig: r.jwtConfig, Provider: r.provider, } - refreshToken, refreshExpiresAt, err := req.RefreshJWT() + refreshToken, refreshExpiresAt, err := request.RefreshJWT() if err != nil { r.log.Error(err) return nil, refreshError @@ -242,6 +264,14 @@ func (r *request) newRefreshToken() (*userApp.NewRefreshTokenResult, error) { return nil, refreshError } + http.SetCookie(res, &http.Cookie{ + Name: auth.RefreshToken, + Value: refreshToken, + MaxAge: int(r.jwtConfig.RefreshExpiresIn.Seconds()), + Path: "/", + HttpOnly: true, + }) + data := &userApp.RefreshToken{ Refresh: &userApp.Token{ Token: refreshToken, @@ -252,3 +282,47 @@ func (r *request) newRefreshToken() (*userApp.NewRefreshTokenResult, error) { return &userApp.NewRefreshTokenResult{Data: data}, nil } + +func (s *UserService) GetAccessToken(res http.ResponseWriter, req *http.Request) { + + c, err := req.Cookie("accessToken") + if err == http.ErrNoCookie { + http.Error(res, err.Error(), http.StatusUnauthorized) + return + } + + accessToken := c.Value + + result := userApp.ExitingAccessToken{Data: accessToken} + + res.WriteHeader(http.StatusOK) + if err := json.NewEncoder(res).Encode(result); err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return + } +} + +func (s *UserService) Logout(res http.ResponseWriter, req *http.Request) { + + // Unset the cookie + deleteCookie(res, auth.AccessToken) + deleteCookie(res, auth.RefreshToken) + + result := userApp.ClearCookies{Data: true} + + res.WriteHeader(http.StatusOK) + if err := json.NewEncoder(res).Encode(result); err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return + } +} + +func deleteCookie(res http.ResponseWriter, name string) { + cookie := &http.Cookie{ + Name: name, + MaxAge: -1, + Path: "/", + HttpOnly: true, + } + http.SetCookie(res, cookie) +} diff --git a/api/pkg/user/service/user_test.go b/api/pkg/user/service/user_test.go index aad89dbb1d..bf90901815 100644 --- a/api/pkg/user/service/user_test.go +++ b/api/pkg/user/service/user_test.go @@ -42,6 +42,8 @@ func TestInfo(t *testing.T) { if err != nil { t.Fatal(err) } + // add cookie in the request with accessToken name + req.AddCookie(tc.CreateCookie("accessToken", accessToken)) res := httptest.NewRecorder() @@ -50,13 +52,14 @@ func TestInfo(t *testing.T) { JwtConfig: tc.JWTConfig(), } - req.Header.Set("Authorization", accessToken) handler := http.HandlerFunc(jwt.JWTAuth(userSvc.Info)) + assert.NoError(t, err) handler.ServeHTTP(res, req) var u *userApp.InfoResult + err = json.Unmarshal(res.Body.Bytes(), &u) assert.NoError(t, err) @@ -81,6 +84,9 @@ func TestRefreshAccessToken(t *testing.T) { t.Fatal(err) } + // add cookie in the request with refreshToken name + req.AddCookie(tc.CreateCookie("refreshToken", refreshToken)) + res := httptest.NewRecorder() userSvc := New(tc) @@ -88,7 +94,6 @@ func TestRefreshAccessToken(t *testing.T) { JwtConfig: tc.JWTConfig(), } - req.Header.Set("Authorization", refreshToken) handler := http.HandlerFunc(jwt.JWTAuth(userSvc.RefreshAccessToken)) assert.NoError(t, err) @@ -126,6 +131,10 @@ func TestRefreshAccessToken_RefreshTokenChecksumIsDifferent(t *testing.T) { if err != nil { t.Fatal(err) } + + // add cookie in the request with refreshToken name + req.AddCookie(tc.CreateCookie("refreshToken", refreshToken)) + res := httptest.NewRecorder() userSvc := New(tc) @@ -133,7 +142,7 @@ func TestRefreshAccessToken_RefreshTokenChecksumIsDifferent(t *testing.T) { JwtConfig: tc.JWTConfig(), } - req.Header.Set("Authorization", refreshToken) + // req.Header.Set("Authorization", refreshToken) handler := http.HandlerFunc(jwt.JWTAuth(userSvc.RefreshAccessToken)) handler.ServeHTTP(res, req) @@ -157,6 +166,9 @@ func TestNewRefreshToken(t *testing.T) { t.Fatal(err) } + // add cookie in the request with accessToken name + req.AddCookie(tc.CreateCookie("refreshToken", refreshToken)) + res := httptest.NewRecorder() userSvc := New(tc) @@ -164,7 +176,6 @@ func TestNewRefreshToken(t *testing.T) { JwtConfig: tc.JWTConfig(), } - req.Header.Set("Authorization", refreshToken) handler := http.HandlerFunc(jwt.JWTAuth(userSvc.NewRefreshToken)) assert.NoError(t, err) @@ -192,6 +203,7 @@ func TestNewRefreshToken_RefreshTokenChecksumIsDifferent(t *testing.T) { // user refresh token testUser, refreshToken, err := tc.RefreshTokenForUser("foo", "foo@bar.com") + assert.Equal(t, testUser.Email, "foo@bar.com") assert.NoError(t, err) @@ -202,6 +214,10 @@ func TestNewRefreshToken_RefreshTokenChecksumIsDifferent(t *testing.T) { if err != nil { t.Fatal(err) } + + // add cookie in the request with accessToken name + req.AddCookie(tc.CreateCookie("refreshToken", refreshToken)) + res := httptest.NewRecorder() userSvc := New(tc) @@ -209,9 +225,88 @@ func TestNewRefreshToken_RefreshTokenChecksumIsDifferent(t *testing.T) { JwtConfig: tc.JWTConfig(), } - req.Header.Set("Authorization", refreshToken) handler := http.HandlerFunc(jwt.JWTAuth(userSvc.NewRefreshToken)) handler.ServeHTTP(res, req) assert.Equal(t, res.Body.String(), "invalid refresh token\n") } + +func TestLogout(t *testing.T) { + tc := testutils.Setup(t) + testutils.LoadFixtures(t, tc.FixturePath()) + + testUser, accessToken, err := tc.UserWithScopes("abc", "abc@bar.com", "rating:read", "rating:write") + assert.Equal(t, testUser.Email, "abc@bar.com") + assert.NoError(t, err) + + // Mocks the time + jwt.TimeFunc = testutils.Now + + req, err := http.NewRequest("GET", "/user/logout", nil) + if err != nil { + t.Fatal(err) + } + + // add cookie in the request with accessToken name + req.AddCookie(tc.CreateCookie("accessToken", accessToken)) + + res := httptest.NewRecorder() + + userSvc := New(tc) + jwt := UserService{ + JwtConfig: tc.JWTConfig(), + } + + handler := http.HandlerFunc(jwt.JWTAuth(userSvc.Logout)) + + assert.NoError(t, err) + + handler.ServeHTTP(res, req) + + var u *userApp.ClearCookies + + err = json.Unmarshal(res.Body.Bytes(), &u) + assert.NoError(t, err) + + assert.Equal(t, true, u.Data) +} + +func TestGetAccessToken(t *testing.T) { + tc := testutils.Setup(t) + testutils.LoadFixtures(t, tc.FixturePath()) + + testUser, accessToken, err := tc.UserWithScopes("abc", "abc@bar.com", "rating:read", "rating:write") + assert.Equal(t, testUser.Email, "abc@bar.com") + assert.NoError(t, err) + + // Mocks the time + jwt.TimeFunc = testutils.Now + + req, err := http.NewRequest("GET", "/user/accesstoken", nil) + if err != nil { + t.Fatal(err) + } + + // add cookie in the request with accessToken name + req.AddCookie(tc.CreateCookie("accessToken", accessToken)) + + res := httptest.NewRecorder() + + userSvc := New(tc) + jwt := UserService{ + JwtConfig: tc.JWTConfig(), + } + + handler := http.HandlerFunc(jwt.JWTAuth(userSvc.GetAccessToken)) + + assert.NoError(t, err) + + handler.ServeHTTP(res, req) + + var u *userApp.ExitingAccessToken + + err = json.Unmarshal(res.Body.Bytes(), &u) + assert.NoError(t, err) + + assert.Equal(t, accessToken, u.Data) +}