From b79fbb231ba1872177ed98b6acae14ac1ac8c2fc Mon Sep 17 00:00:00 2001 From: Fergal Mc Carthy Date: Fri, 2 Aug 2024 16:05:06 -0400 Subject: [PATCH] HTTP 401 responses should include WWW-Authentciate header When generating a HTTP 401 StatusUnauthorized response to a telemetry client the response should include an appropriate WWW-Authenticate header indicating to the client what action it should take to address it's lack of authorization. The generated WWW-Authenticate header uses a custom "Bearer" challenge specifying a realm of "suse-telemetry-service" with a scope of either "authenticate" or "register" indicating the appropriate action which the client needs to take. Fixes: #47 --- app/app.go | 19 ++++++++++++++++++ app/handler_authenticate.go | 14 ++++++++----- app/handler_report.go | 39 ++++++++++++++++++++++++++----------- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/app/app.go b/app/app.go index 9af4006..de6ecb5 100644 --- a/app/app.go +++ b/app/app.go @@ -81,6 +81,25 @@ func (ar *AppRequest) ContentTypeJSON() { ar.ContentType("application/json") } +func (ar *AppRequest) SetWwwAuthenticate(challenge, realm, scope string) { + ar.SetHeader( + "WWW-Authenticate", + fmt.Sprintf(`%s realm="%s" scope="%s"`, challenge, realm, scope), + ) +} + +func (ar *AppRequest) SetWwwAuthScope(scope string) { + ar.SetWwwAuthenticate("Bearer", "suse-telemetry-service", scope) +} + +func (ar *AppRequest) SetWwwAuthReauth() { + ar.SetWwwAuthScope("authenticate") +} + +func (ar *AppRequest) SetWwwAuthRegister() { + ar.SetWwwAuthScope("register") +} + func (ar *AppRequest) Status(statusCode int) { ar.Log.Debug("Response status", slog.Int("code", statusCode)) ar.W.WriteHeader(statusCode) diff --git a/app/handler_authenticate.go b/app/handler_authenticate.go index 72fe11b..5ac4fd8 100644 --- a/app/handler_authenticate.go +++ b/app/handler_authenticate.go @@ -29,7 +29,8 @@ func (a *App) AuthenticateClient(ar *AppRequest) { return } if caReq.ClientId <= 0 { - ar.ErrorResponse(http.StatusBadRequest, "Invalid ClientId value provided") + ar.SetWwwAuthRegister() + ar.ErrorResponse(http.StatusUnauthorized, "Invalid ClientId value provided") return } ar.Log.Debug("Unmarshaled", slog.Any("caReq", &caReq)) @@ -45,8 +46,8 @@ func (a *App) AuthenticateClient(ar *AppRequest) { // confirm that the client has been registered if !client.Exists() { - // TODO: Set WWW-Authenticate header appropriately, per - // https://www.rfc-editor.org/rfc/rfc9110.html#name-www-authenticate + // client needs to register + ar.SetWwwAuthRegister() ar.ErrorResponse(http.StatusUnauthorized, "Client not registered") return } @@ -59,12 +60,15 @@ func (a *App) AuthenticateClient(ar *AppRequest) { slog.String("Req Hash", caReq.InstIdHash.String()), slog.String("DB Hash", instIdHash.String()), ) - // TODO: Set WWW-Authenticate header appropriately, per - // https://www.rfc-editor.org/rfc/rfc9110.html#name-www-authenticate + // client needs to re-register + ar.SetWwwAuthRegister() ar.ErrorResponse(http.StatusUnauthorized, "ClientInstanceId mismatch") return } + // TODO: return existing token if remaining duration is >= half of + // a new tokens duration + // create a new token for the client client.AuthToken, err = a.AuthManager.CreateToken() if err != nil { diff --git a/app/handler_report.go b/app/handler_report.go index ca4ae80..76025ae 100644 --- a/app/handler_report.go +++ b/app/handler_report.go @@ -15,12 +15,24 @@ import ( func (a *App) ReportTelemetry(ar *AppRequest) { ar.Log.Info("Processing") - // verify that a valid authtoken has been provided + // retrieve required headers + hdrClientId := ar.GetClientId() token := ar.GetAuthToken() + + // missing clientId or token suggests client needs to register + if (hdrClientId == "") || (token == "") { + // client needs to register + ar.SetWwwAuthRegister() + ar.ErrorResponse(http.StatusUnauthorized, "Client registration required") + return + } + + // verify that a valid authtoken has been provided if err := a.AuthManager.VerifyToken(token); err != nil { - // TODO: Set WWW-Authenticate header appropriately, per - // https://www.rfc-editor.org/rfc/rfc9110.html#name-www-authenticate - ar.ErrorResponse(http.StatusUnauthorized, "Missing or Invalid Authorization") + // client needs to re-authenticate + ar.SetWwwAuthReauth() + ar.ErrorResponse(http.StatusUnauthorized, "Invalid Authorization") + return } ar.Log.Debug( @@ -29,12 +41,12 @@ func (a *App) ReportTelemetry(ar *AppRequest) { ) // verify that the provided client id is a valid number - hdrClientId := ar.GetClientId() clientId, err := strconv.ParseInt(hdrClientId, 0, 64) if err != nil { - // TODO: Set WWW-Authenticate header appropriately, per - // https://www.rfc-editor.org/rfc/rfc9110.html#name-www-authenticate + // client needs to register + ar.SetWwwAuthRegister() ar.ErrorResponse(http.StatusUnauthorized, "Invalid Client Id") + return } // verify that the request is from a registered client @@ -45,17 +57,22 @@ func (a *App) ReportTelemetry(ar *AppRequest) { ar.ErrorResponse(http.StatusInternalServerError, "failed to access DB") return } + if !client.Exists() { - // TODO: Set WWW-Authenticate header appropriately, per - // https://www.rfc-editor.org/rfc/rfc9110.html#name-www-authenticate + // client needs to register + ar.SetWwwAuthRegister() ar.ErrorResponse(http.StatusUnauthorized, "Invalid Client Id") + return } // verify that the provided authtoken matches last authtoken issued to the client if client.AuthToken != token { - // TODO: Set WWW-Authenticate header appropriately, per - // https://www.rfc-editor.org/rfc/rfc9110.html#name-www-authenticate + // TODO detect cloned clients, where InstID matches ClientId, but authtoken will + // will be stale + // client needs to re-authenticate + ar.SetWwwAuthReauth() ar.ErrorResponse(http.StatusUnauthorized, "Invalid Authorization") + return } ar.Log.Debug(