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:
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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"`
|
||||
|
@ -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!
|
||||
|
@ -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 }
|
||||
|
@ -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!]!
|
||||
}
|
||||
|
||||
|
@ -90,6 +90,7 @@ type ProjectRole {
|
||||
|
||||
type MePayload {
|
||||
user: UserAccount!
|
||||
organization: RoleCode
|
||||
teamRoles: [TeamRole!]!
|
||||
projectRoles: [ProjectRole!]!
|
||||
}
|
||||
|
@ -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!
|
||||
|
Reference in New Issue
Block a user