-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/implement backend authentication #68
Feature/implement backend authentication #68
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me do more research before approving this
accessToken := r.Header.Get("Authorization") | ||
session, _ := store.Get(r, accessToken) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typically, the value in this header will be Bearer <token>
. For example, using auth0, typically the frontend will get the token of type Bearer
. They will then form the Bearer <token>
string and pass it to the backend. You can see it from the React example provided directly from Auth0 https://auth0.com/docs/quickstart/spa/react/02-calling-an-api
So you will probably need to do some string parsing to get the token
from the Authorization header.
logger.Error("[Authorization] Unable to verify token", map[string]interface{}{"err": err}) | ||
authError := &api.HandlerError{Error: errors.New("unable to verify token"), Code: http.StatusUnauthorized} | ||
w.WriteHeader(authError.Code) | ||
json.NewEncoder(w).Encode(authError) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
encoding the error like this will not work. see my PR #65 description (or see this stack overflow answer https://stackoverflow.com/questions/44989924/golang-error-types-are-empty-when-encoded-to-json)
backend/src/authenticator/auth.go
Outdated
ClientSecret: os.Getenv("AUTH0_CLIENT_SECRET"), | ||
RedirectURL: os.Getenv("AUTH0_CALLBACK_URL"), | ||
Endpoint: provider.Endpoint(), | ||
Scopes: []string{oidc.ScopeOpenID, "profile"}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we be setting the scope here? I'm not too familiar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was based off their quick start guide so we just stick with this first until changes are required bah since it doesn't seem to create any immediate issues in my mind
… and updated http err handling per PR#73
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Key changes required:
- I believe we should use access tokens instead of ID tokens
- We should also verify the scope of the request, issuer domain as well as the audience of the token.
You can cross reference with the official auth0 Golang guide here
backend/src/authenticator/auth.go
Outdated
} | ||
|
||
func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) { | ||
rawIDToken, ok := token.Extra("id_token").(string) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is this Extra("id_token")
about actually? Can you provide a reference/documentation for me to take a look at the id_token
field that is present in oauth2 JWT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I just read up more on the different token types for JWT. There is an ID token and Access token type. ID tokens should be used for authentication (i.e. a user is a valid user) and access tokens should be used for authorisation (i.e. a user is valid and is authorised to perform a certain action).
from https://auth0.com/docs/secure/tokens/id-tokens: ID Tokens should never be used to obtain direct access to APIs or to make authorization decisions.
So you'll need to consider which APIs require an ID token, and which ones require an Access token and use the correct middleware accordingly. In our scenario, I believe we should be using Access tokens instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The quick start guide I was using is post login when I created the application. The guide only showed how to integrate authentication and not authorisation, hence the discrepancy here I think. Ill just refer to the guide you mentioned.
backend/src/server/middleware.go
Outdated
func IsAuthenticated(next http.Handler) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Add("Content-Type", "application/json") | ||
accessToken := strings.TrimSpace(strings.Replace(r.Header.Get("Authorization"), "Bearer", "", 1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not too sure about the security implications about this method of doing things.
I think it's better to reject if the header doesn't contain the prefix Bearer
, even if the token is correct. A better way is to do a strings.Split
and verify 1) there is a Bearer
prefix 2) there should only be the Bearer
string and the token string after the split
backend/src/server/server.go
Outdated
@@ -35,6 +35,7 @@ func addRoutes(r *mux.Router) { | |||
func New() http.Handler { | |||
router := mux.NewRouter() | |||
addRoutes(router) | |||
router.Use(IsAuthenticated) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have 2 options:
- Create a global middleware that authenticates every request; create a authorisation middleware that we chain on every handler that requires authorisation (such as creating new pipelines, approvals, etc.)
- Don't use a global middleware for authentication. On every handler (that requires authentication), we will chain this authentication middleware.
Method 1 reduces the amount of code to write but it assumes that every handler requires authentication. It might just be better to opt for method 2, even though we have to repeat ourselves every time. Thoughts?
reference: https://auth0.com/blog/id-token-access-token-what-is-the-difference/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thing is I can't think of a situation where authentication is not required. The only possibility would be a server side landing page, but since our backend is purely just API, there should not be any paths where authorisation middleware is not required. In that case, since the only differentiation is between authorisation for different roles, I think we just do two bah
compose.yaml
Outdated
@@ -20,6 +20,7 @@ services: | |||
- be | |||
build: | |||
context: backend | |||
env_file: .env |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you shouldn't need this field as .env
is the default env_file
that will be sourced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't source from the .env for me though. I needed to add it in to see the env variables
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where are you running your docker
command from? your .env
should be located in the root of the project. run your docker
commands from the root as well.
…ncode cannot encode bytes properly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code looks fine except the docker compose file and my comments. But you haven't used the middleware?
backend/src/server/helper.go
Outdated
// HasScope checks whether our claims have a specific scope. | ||
func (c CustomClaims) HasScope(expectedScope string) bool { | ||
result := strings.Split(c.Scope, " ") | ||
for i := range result { | ||
if result[i] == expectedScope { | ||
return true | ||
} | ||
} | ||
|
||
return false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why didn't you keep this method in the middleware.go
file?
backend/src/server/middleware.go
Outdated
jwtValidator.ValidateToken, | ||
jwtmiddleware.WithErrorHandler(errorHandler), | ||
) | ||
logger.Info("Validating", nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we get a better log message here?
backend/src/server/middleware.go
Outdated
logger.Info("Authorizing", nil) | ||
token := r.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims) | ||
claims := token.CustomClaims.(*CustomClaims) | ||
if !claims.HasScope("test") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should the scope still be test
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some minor changes, other than that should be good
backend/src/server/server.go
Outdated
@@ -15,21 +15,21 @@ func addRoutes(r *mux.Router) { | |||
} | |||
|
|||
// Health Check | |||
r.Handle("/api/healthcheck", handleHealthCheck()).Methods("GET") | |||
r.Handle("/api/healthcheck", isAuthenticated(isAuthorisedAdmin(handleHealthCheck()))).Methods("GET") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to authorise the health check.
backend/src/server/server.go
Outdated
r.Handle("/api/pipeline", isAuthenticated(isAuthorisedUser(handleGetAllPipelines(mongoClient)))).Methods("GET") | ||
r.Handle("/api/pipeline/{pipelineId}", isAuthenticated(isAuthorisedUser(handleGetPipeline(mongoClient)))).Methods("GET") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, basic users shouldn't be able to see the pipeline. They should only be able to see the service request that uses the latest version of the pipeline
backend/src/server/middleware.go
Outdated
// Validate does nothing for this example, but we need | ||
// it to satisfy validator.CustomClaims interface. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can remove this comment? or make it more relevant to our use case
…e between admin and user
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Except for the HasPermission
in helper.go
backend/src/server/helper.go
Outdated
// HasPermission checks whether our claims have a specific permission. | ||
// In our case, since we are using this to check if user is admin, will be checking for approve:pipeline_step permission | ||
func (c CustomClaims) HasPermission(expectedPermission string) bool { | ||
result := strings.Split(c.Permissions, ",") | ||
for i := range result { | ||
if result[i] == expectedPermission { | ||
return true | ||
} | ||
} | ||
|
||
return false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wait why you move this here? why didn't you keep it with middleware.go
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this function is just a helper function to check if a permission exists, to me not really a middleware exactly
Description
In this PR, I created the authentication and authorization middleware to authenticate requests in the backend.
*Assumptions: