{"record":{"id":"c17103d3d59edd8b","repo":"dotnet/eShop","slug":"unauthenticated","errorCode":"Unauthenticated","errorMessage":"The caller is not authenticated.","messagePattern":"The caller is not authenticated\\.","errorType":"exception","errorClass":"RpcException","httpStatus":null,"severity":"error","filePath":"src/Basket.API/Grpc/BasketService.cs","lineNumber":72,"sourceCode":"        }\n\n        return MapToCustomerBasketResponse(response);\n    }\n\n    public override async Task<DeleteBasketResponse> DeleteBasket(DeleteBasketRequest request, ServerCallContext context)\n    {\n        var userId = context.GetUserIdentity();\n        if (string.IsNullOrEmpty(userId))\n        {\n            ThrowNotAuthenticated();\n        }\n\n        await repository.DeleteBasketAsync(userId);\n        return new();\n    }\n\n    [DoesNotReturn]\n    private static void ThrowNotAuthenticated() => throw new RpcException(new Status(StatusCode.Unauthenticated, \"The caller is not authenticated.\"));\n\n    [DoesNotReturn]\n    private static void ThrowBasketDoesNotExist(string userId) => throw new RpcException(new Status(StatusCode.NotFound, $\"Basket with buyer id {userId} does not exist\"));\n\n    private static CustomerBasketResponse MapToCustomerBasketResponse(CustomerBasket customerBasket)\n    {\n        var response = new CustomerBasketResponse();\n\n        foreach (var item in customerBasket.Items)\n        {\n            response.Items.Add(new BasketItem()\n            {\n                ProductId = item.ProductId,\n                Quantity = item.Quantity,\n            });\n        }\n\n        return response;","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/dotnet/eShop/blob/9b4f9434f46fdc5c1a6e9e936af2868340cdbc48/src/Basket.API/Grpc/BasketService.cs#L54-L90","documentation":"Thrown by the Basket gRPC service's DeleteBasket (and UpdateBasket) RPC when context.GetUserIdentity() returns null or empty. The service reads the caller's identity from the gRPC request metadata (Authorization header / JWT sub claim); with no resolvable identity it refuses to mutate state and throws an RpcException with gRPC StatusCode.Unauthenticated. It is an authentication/authorization boundary failure, not a domain-logic error.","triggerScenarios":"Calling Basket.BasketClient.DeleteBasketAsync or UpdateBasketAsync from a gRPC client with no Authorization metadata, with a token whose 'sub'/'nameidentifier' claim is absent, or with a misrouted request that bypasses the YARP/Envoy auth gateway. Anonymous calls are only allowed on GetBasket; the two mutating RPCs are NOT marked [AllowAnonymous].","commonSituations":"JWT bearer scheme not wired into the gRPC client channel; token issued without a sub claim; the client forwards metadata via a header name the server extension does not look for; dev environment hitting Basket.API directly (skipping the BFF/API gateway that injects the token); clock-skewed/expired token that the gateway rejects before the claim reaches the service.","solutions":["Ensure the gRPC client attaches a valid JWT: call callOptions = callOptions.WithCredentials(CallCredentials.FromInterceptor((_, metadata) => metadata.Add(\"Authorization\", $\"Bearer {token}\"))) on every mutating call.","Verify the token contains the claim key GetUserIdentity reads (typically 'sub' or ClaimTypes.NameIdentifier); inspect the decoded JWT payload.","Confirm the request is routed through the API gateway/BFF that performs token validation and forwards the identity, rather than calling Basket.API directly without auth.","If anonymous delete is genuinely intended, re-evaluate the design — DeleteBasket intentionally requires identity; do not mark it [AllowAnonymous]."],"exampleFix":"// before\nvar client = new Basket.BasketClient(channel);\nawait client.DeleteBasketAsync(new DeleteBasketRequest());\n\n// after\nvar headers = new Metadata { { \"Authorization\", $\"Bearer {jwt}\" } };\nawait client.DeleteBasketAsync(new DeleteBasketRequest(), headers);","handlingStrategy":"try-catch","validationCode":"var userId = ParseUserIdentityFromToken(jwt); // resolve the sub/nameid claim locally\nif (string.IsNullOrEmpty(userId))\n    return Error(\"Sign-in required before modifying the basket.\");","typeGuard":"// not applicable — identity is resolved server-side from metadata; client-side type guard is on the JWT payload\nstatic bool HasSubjectClaim(string jwt) {\n    var payload = JwtDecoder.Decode(jwt);\n    return !string.IsNullOrEmpty(payload?.Sub);\n}","tryCatchPattern":"try {\n    await basketClient.DeleteBasketAsync(req, headers);\n} catch (RpcException ex) when (ex.StatusCode == StatusCode.Unauthenticated) {\n    // re-authenticate the user, refresh the token, then retry once\n    await RefreshTokenAsync();\n}","preventionTips":["Always build call options through a helper that injects a current Authorization header.","Centralize token refresh so an expired token never reaches a mutating RPC.","Route mutating calls through the authenticated gateway rather than direct to Basket.API."],"tags":["grpc","authentication","basket-api","authorization","jwt"],"backgroundTag":null,"analyzedSha":"9b4f9434f46fdc5c1a6e9e936af2868340cdbc48","analyzedAt":"2026-08-13T19:29:36.594Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}