refactor: replace refresh & access token with auth token only

changes authentication to no longer use a refresh token & access token
for accessing protected endpoints. Instead only an auth token is used.

Before the login flow was:

Login -> get refresh (stored as HttpOnly cookie) + access token (stored in memory) ->
  protected endpoint request (attach access token as Authorization header) -> access token expires in
  15 minutes, so use refresh token to obtain new one when that happens

now it looks like this:

Login -> get auth token (stored as HttpOnly cookie) -> make protected endpont
request (token sent)

the reasoning for using the refresh + access token was to reduce DB
calls, but in the end I don't think its worth the hassle.
This commit is contained in:
Jordan Knott
2021-04-28 21:32:19 -05:00
parent 3392b3345d
commit 229a53fa0a
47 changed files with 3989 additions and 3717 deletions

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,6 @@ import (
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/google/uuid"
"github.com/jordanknott/taskcafe/internal/auth"
"github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/logger"
"github.com/jordanknott/taskcafe/internal/utils"
@ -34,15 +33,18 @@ func NewHandler(repo db.Repository, emailConfig utils.EmailConfig) http.Handler
},
}
c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) {
role, ok := GetUserRole(ctx)
if !ok {
return nil, errors.New("user ID is missing")
}
if role == "admin" {
return next(ctx)
} else if level == ActionLevelOrg {
return nil, errors.New("must be an org admin")
}
/*
TODO: add permission check
role, ok := GetUserRole(ctx)
if !ok {
return nil, errors.New("user ID is missing")
}
if role == "admin" {
return next(ctx)
} else if level == ActionLevelOrg {
return nil, errors.New("must be an org admin")
}
*/
var subjectID uuid.UUID
in := graphql.GetFieldContext(ctx).Args["input"]
@ -76,7 +78,7 @@ func NewHandler(repo db.Repository, emailConfig utils.EmailConfig) http.Handler
// TODO: add config setting to disable personal projects
return next(ctx)
}
subjectID, ok = subjectField.Interface().(uuid.UUID)
subjectID, ok := subjectField.Interface().(uuid.UUID)
if !ok {
logger.New(ctx).Error("error while casting subject UUID")
return nil, errors.New("error while casting subject uuid")
@ -190,23 +192,10 @@ func GetUserID(ctx context.Context) (uuid.UUID, bool) {
return userID, ok
}
// GetUserRole retrieves the user role out of a context
func GetUserRole(ctx context.Context) (auth.Role, bool) {
role, ok := ctx.Value(utils.OrgRoleKey).(auth.Role)
return role, ok
}
// GetUser retrieves both the user id & user role out of a context
func GetUser(ctx context.Context) (uuid.UUID, auth.Role, bool) {
func GetUser(ctx context.Context) (uuid.UUID, bool) {
userID, userOK := GetUserID(ctx)
role, roleOK := GetUserRole(ctx)
return userID, role, userOK && roleOK
}
// GetRestrictedMode retrieves the restricted mode code out of a context
func GetRestrictedMode(ctx context.Context) (auth.RestrictedMode, bool) {
restricted, ok := ctx.Value(utils.RestrictedModeKey).(auth.RestrictedMode)
return restricted, ok
return userID, userOK
}
// GetProjectRoles retrieves the team & project role for the given project ID

View File

@ -255,6 +255,7 @@ type LogoutUser struct {
type MePayload struct {
User *db.UserAccount `json:"user"`
Organization *RoleCode `json:"organization"`
TeamRoles []TeamRole `json:"teamRoles"`
ProjectRoles []ProjectRole `json:"projectRoles"`
}
@ -312,10 +313,6 @@ type NewProjectLabel struct {
Name *string `json:"name"`
}
type NewRefreshToken struct {
UserID uuid.UUID `json:"userID"`
}
type NewTask struct {
TaskGroupID uuid.UUID `json:"taskGroupID"`
Name string `json:"name"`
@ -382,6 +379,12 @@ type ProfileIcon struct {
BgColor *string `json:"bgColor"`
}
type ProjectPermission struct {
Team RoleCode `json:"team"`
Project RoleCode `json:"project"`
Org RoleCode `json:"org"`
}
type ProjectRole struct {
ProjectID uuid.UUID `json:"projectID"`
RoleCode RoleCode `json:"roleCode"`
@ -435,6 +438,11 @@ type TaskPositionUpdate struct {
Position float64 `json:"position"`
}
type TeamPermission struct {
Team RoleCode `json:"team"`
Org RoleCode `json:"org"`
}
type TeamRole struct {
TeamID uuid.UUID `json:"teamID"`
RoleCode RoleCode `json:"roleCode"`

View File

@ -50,13 +50,6 @@ type Member {
member: MemberList!
}
type RefreshToken {
id: ID!
userId: UUID!
expiresAt: Time!
createdAt: Time!
}
type Role {
code: String!
name: String!
@ -97,6 +90,7 @@ type Team {
id: ID!
createdAt: Time!
name: String!
permission: TeamPermission!
members: [Member!]!
}
@ -106,6 +100,17 @@ type InvitedMember {
invitedOn: Time!
}
type TeamPermission {
team: RoleCode!
org: RoleCode!
}
type ProjectPermission {
team: RoleCode!
project: RoleCode!
org: RoleCode!
}
type Project {
id: ID!
createdAt: Time!
@ -114,6 +119,7 @@ type Project {
taskGroups: [TaskGroup!]!
members: [Member!]!
invitedMembers: [InvitedMember!]!
permission: ProjectPermission!
labels: [ProjectLabel!]!
}
@ -314,6 +320,7 @@ type ProjectRole {
type MePayload {
user: UserAccount!
organization: RoleCode
teamRoles: [TeamRole!]!
projectRoles: [ProjectRole!]!
}
@ -881,7 +888,6 @@ type UpdateTeamMemberRolePayload {
}
extend type Mutation {
createRefreshToken(input: NewRefreshToken!): RefreshToken!
createUserAccount(input: NewUserAccount!):
UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
deleteUserAccount(input: DeleteUserAccount!):
@ -954,10 +960,6 @@ type UpdateUserRolePayload {
user: UserAccount!
}
input NewRefreshToken {
userID: UUID!
}
input NewUserAccount {
username: String!
email: String!

View File

@ -13,7 +13,6 @@ import (
"github.com/google/uuid"
"github.com/jinzhu/now"
"github.com/jordanknott/taskcafe/internal/auth"
"github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/logger"
"github.com/jordanknott/taskcafe/internal/utils"
@ -864,11 +863,12 @@ func (r *mutationResolver) DeleteTeam(ctx context.Context, input DeleteTeam) (*D
}
func (r *mutationResolver) CreateTeam(ctx context.Context, input NewTeam) (*db.Team, error) {
_, role, ok := GetUser(ctx)
_, ok := GetUser(ctx)
if !ok {
return &db.Team{}, nil
}
if role == auth.RoleAdmin {
// if role == auth.RoleAdmin { // TODO: add permision check
if true {
createdAt := time.Now().UTC()
team, err := r.Repository.CreateTeam(ctx, db.CreateTeamParams{OrganizationID: input.OrganizationID, CreatedAt: createdAt, Name: input.Name})
return &team, err
@ -944,20 +944,13 @@ func (r *mutationResolver) DeleteTeamMember(ctx context.Context, input DeleteTea
return &DeleteTeamMemberPayload{TeamID: input.TeamID, UserID: input.UserID}, err
}
func (r *mutationResolver) CreateRefreshToken(ctx context.Context, input NewRefreshToken) (*db.RefreshToken, error) {
userID := uuid.MustParse("0183d9ab-d0ed-4c9b-a3df-77a0cdd93dca")
refreshCreatedAt := time.Now().UTC()
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
refreshToken, err := r.Repository.CreateRefreshToken(ctx, db.CreateRefreshTokenParams{userID, refreshCreatedAt, refreshExpiresAt})
return &refreshToken, err
}
func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserAccount) (*db.UserAccount, error) {
_, role, ok := GetUser(ctx)
_, ok := GetUser(ctx)
if !ok {
return &db.UserAccount{}, nil
}
if role != auth.RoleAdmin {
// if role != auth.RoleAdmin { TODO: add permsion check
if true {
return &db.UserAccount{}, &gqlerror.Error{
Message: "Must be an organization admin",
Extensions: map[string]interface{}{
@ -984,11 +977,12 @@ func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserA
}
func (r *mutationResolver) DeleteUserAccount(ctx context.Context, input DeleteUserAccount) (*DeleteUserAccountPayload, error) {
_, role, ok := GetUser(ctx)
_, ok := GetUser(ctx)
if !ok {
return &DeleteUserAccountPayload{Ok: false}, nil
}
if role != auth.RoleAdmin {
// if role != auth.RoleAdmin { TODO: add permision check
if true {
return &DeleteUserAccountPayload{Ok: false}, &gqlerror.Error{
Message: "User not found",
Extensions: map[string]interface{}{
@ -1030,7 +1024,7 @@ func (r *mutationResolver) DeleteInvitedUserAccount(ctx context.Context, input D
}
func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bool, error) {
err := r.Repository.DeleteRefreshTokenByUserID(ctx, input.UserID)
err := r.Repository.DeleteAuthTokenByUserID(ctx, input.UserID)
return true, err
}
@ -1059,11 +1053,12 @@ func (r *mutationResolver) UpdateUserPassword(ctx context.Context, input UpdateU
}
func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error) {
_, role, ok := GetUser(ctx)
_, ok := GetUser(ctx)
if !ok {
return &UpdateUserRolePayload{}, nil
}
if role != auth.RoleAdmin {
// if role != auth.RoleAdmin { TODO: add permision check
if true {
return &UpdateUserRolePayload{}, &gqlerror.Error{
Message: "User not found",
Extensions: map[string]interface{}{
@ -1211,6 +1206,10 @@ func (r *projectResolver) InvitedMembers(ctx context.Context, obj *db.Project) (
return invited, err
}
func (r *projectResolver) Permission(ctx context.Context, obj *db.Project) (*ProjectPermission, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *projectResolver) Labels(ctx context.Context, obj *db.Project) ([]db.ProjectLabel, error) {
labels, err := r.Repository.GetProjectLabelsForProject(ctx, obj.ProjectID)
return labels, err
@ -1296,7 +1295,7 @@ func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*db.Task,
}
func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]db.Project, error) {
userID, orgRole, ok := GetUser(ctx)
userID, ok := GetUser(ctx)
if !ok {
logger.New(ctx).Info("user id was not found from middleware")
return []db.Project{}, nil
@ -1309,11 +1308,14 @@ func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]
var teams []db.Team
var err error
/* TODO: add permsion check
if orgRole == "admin" {
teams, err = r.Repository.GetAllTeams(ctx)
} else {
teams, err = r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
}
*/
teams, err = r.Repository.GetAllTeams(ctx)
projects := make(map[string]db.Project)
for _, team := range teams {
@ -1359,15 +1361,18 @@ func (r *queryResolver) FindTeam(ctx context.Context, input FindTeam) (*db.Team,
}
func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
userID, orgRole, ok := GetUser(ctx)
userID, ok := GetUser(ctx)
if !ok {
logger.New(ctx).Error("userID or org role does not exist")
return []db.Team{}, errors.New("internal error")
}
if orgRole == "admin" {
return r.Repository.GetAllTeams(ctx)
}
/*
TODO: add permision check
if orgRole == "admin" {
return r.Repository.GetAllTeams(ctx)
}
*/
teams := make(map[string]db.Team)
adminTeams, err := r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
@ -1596,10 +1601,6 @@ func (r *queryResolver) SearchMembers(ctx context.Context, input MemberSearchFil
return results, nil
}
func (r *refreshTokenResolver) ID(ctx context.Context, obj *db.RefreshToken) (uuid.UUID, error) {
return obj.TokenID, nil
}
func (r *taskResolver) ID(ctx context.Context, obj *db.Task) (uuid.UUID, error) {
return obj.TaskID, nil
}
@ -1848,6 +1849,10 @@ func (r *teamResolver) ID(ctx context.Context, obj *db.Team) (uuid.UUID, error)
return obj.TeamID, nil
}
func (r *teamResolver) Permission(ctx context.Context, obj *db.Team) (*TeamPermission, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, error) {
members := []Member{}
@ -1966,9 +1971,6 @@ func (r *Resolver) ProjectLabel() ProjectLabelResolver { return &projectLabelRes
// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
// RefreshToken returns RefreshTokenResolver implementation.
func (r *Resolver) RefreshToken() RefreshTokenResolver { return &refreshTokenResolver{r} }
// Task returns TaskResolver implementation.
func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
@ -2005,7 +2007,6 @@ type organizationResolver struct{ *Resolver }
type projectResolver struct{ *Resolver }
type projectLabelResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type refreshTokenResolver struct{ *Resolver }
type taskResolver struct{ *Resolver }
type taskActivityResolver struct{ *Resolver }
type taskChecklistResolver struct{ *Resolver }

View File

@ -50,13 +50,6 @@ type Member {
member: MemberList!
}
type RefreshToken {
id: ID!
userId: UUID!
expiresAt: Time!
createdAt: Time!
}
type Role {
code: String!
name: String!
@ -97,6 +90,7 @@ type Team {
id: ID!
createdAt: Time!
name: String!
permission: TeamPermission!
members: [Member!]!
}
@ -106,6 +100,17 @@ type InvitedMember {
invitedOn: Time!
}
type TeamPermission {
team: RoleCode!
org: RoleCode!
}
type ProjectPermission {
team: RoleCode!
project: RoleCode!
org: RoleCode!
}
type Project {
id: ID!
createdAt: Time!
@ -114,6 +119,7 @@ type Project {
taskGroups: [TaskGroup!]!
members: [Member!]!
invitedMembers: [InvitedMember!]!
permission: ProjectPermission!
labels: [ProjectLabel!]!
}

View File

@ -90,6 +90,7 @@ type ProjectRole {
type MePayload {
user: UserAccount!
organization: RoleCode
teamRoles: [TeamRole!]!
projectRoles: [ProjectRole!]!
}

View File

@ -1,5 +1,4 @@
extend type Mutation {
createRefreshToken(input: NewRefreshToken!): RefreshToken!
createUserAccount(input: NewUserAccount!):
UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
deleteUserAccount(input: DeleteUserAccount!):
@ -72,10 +71,6 @@ type UpdateUserRolePayload {
user: UserAccount!
}
input NewRefreshToken {
userID: UUID!
}
input NewUserAccount {
username: String!
email: String!