github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/pkg/interceptors/interceptors.go (about) 1 /*This file is part of kuberpult. 2 3 Kuberpult is free software: you can redistribute it and/or modify 4 it under the terms of the Expat(MIT) License as published by 5 the Free Software Foundation. 6 7 Kuberpult is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 MIT License for more details. 11 12 You should have received a copy of the MIT License 13 along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>. 14 15 Copyright 2023 freiheit.com*/ 16 17 package interceptors 18 19 import ( 20 "context" 21 "fmt" 22 "net/http" 23 24 "github.com/MicahParks/keyfunc/v2" 25 "github.com/freiheit-com/kuberpult/pkg/auth" 26 "google.golang.org/api/idtoken" 27 "google.golang.org/grpc" 28 "google.golang.org/grpc/codes" 29 "google.golang.org/grpc/metadata" 30 "google.golang.org/grpc/status" 31 ) 32 33 // authorize returns an error when the authentication failed 34 // Note that it may return (nil,nil) if the authentication was ok, but had no userdata. 35 // Never returns the default user 36 func authorize(ctx context.Context, jwks *keyfunc.JWKS, clientId string, tenantId string) (*auth.User, error) { 37 md, ok := metadata.FromIncomingContext(ctx) 38 if !ok { 39 return nil, status.Errorf(codes.InvalidArgument, "Retrieving metadata failed") 40 } 41 42 authHeader, ok := md["authorization"] 43 if !ok { 44 return nil, status.Errorf(codes.Unauthenticated, "Authorization token not supplied") 45 } 46 47 token := authHeader[0] 48 claims, err := auth.ValidateToken(token, jwks, clientId, tenantId) 49 if err != nil { 50 return nil, status.Errorf(codes.Unauthenticated, "Invalid authorization token provided") 51 } 52 // here, everything is valid, but we way still have empty strings, so we use the defaultUser here 53 var u *auth.User = nil 54 if _, ok := claims["aud"]; ok && claims["aud"] == clientId { 55 u = &auth.User{ 56 DexAuthContext: nil, 57 Email: claims["email"].(string), 58 Name: claims["name"].(string), 59 } 60 } 61 62 return u, nil 63 } 64 65 func UnaryAuthInterceptor(ctx context.Context, 66 req interface{}, 67 info *grpc.UnaryServerInfo, 68 handler grpc.UnaryHandler, 69 jwks *keyfunc.JWKS, 70 clientId string, 71 tenantId string) (interface{}, error) { 72 if info.FullMethod != "/api.v1.FrontendConfigService/GetConfig" { 73 _, err := authorize(ctx, jwks, clientId, tenantId) 74 if err != nil { 75 return nil, err 76 } 77 } 78 h, err := handler(ctx, req) 79 return h, err 80 } 81 82 func StreamAuthInterceptor( 83 srv interface{}, 84 stream grpc.ServerStream, 85 info *grpc.StreamServerInfo, 86 handler grpc.StreamHandler, 87 jwks *keyfunc.JWKS, 88 clientId string, 89 tenantId string, 90 91 ) error { 92 _, err := authorize(stream.Context(), jwks, clientId, tenantId) 93 if err != nil { 94 return err 95 } 96 return handler(srv, stream) 97 } 98 99 // GoogleIAPInterceptor intercepts HTTP calls to the frontend service. 100 // If the user us not logged in or no JWT is found, an Unauthenticated error is returned. 101 func GoogleIAPInterceptor( 102 w http.ResponseWriter, 103 req *http.Request, 104 httpHandler http.HandlerFunc, 105 backendServiceId, gkeProjectNumber string, 106 ) { 107 iapJWT := req.Header.Get("X-Goog-IAP-JWT-Assertion") 108 if iapJWT == "" { 109 http.Error(w, "iap.jwt header was not found or doesn't exist", http.StatusUnauthorized) 110 return 111 } 112 113 aud := fmt.Sprintf("/projects/%s/global/backendServices/%s", gkeProjectNumber, backendServiceId) 114 // NOTE: currently we just validate that the token exists, but no handlers are using data from the payload. 115 // This might change in the future. 116 _, err := idtoken.Validate(req.Context(), iapJWT, aud) 117 if err != nil { 118 http.Error(w, "iap.jwt could not be validated", http.StatusUnauthorized) 119 return 120 } 121 httpHandler(w, req) 122 } 123 124 // DexLoginInterceptor intercepts HTTP calls to the frontend service. 125 // DexLoginInterceptor must only be used if dex is enabled. 126 // If the user us not logged in, it redirected the calls to the Dex login page. 127 // If the user is already logged in, proceeds with the request. 128 func DexLoginInterceptor( 129 w http.ResponseWriter, 130 req *http.Request, 131 httpHandler http.HandlerFunc, 132 clientID, baseURL string, 133 ) { 134 role, err := auth.VerifyToken(req.Context(), req, clientID, baseURL) 135 if err != nil { 136 // If user is not authenticated redirect to the login page. 137 http.Redirect(w, req, auth.LoginPATH, http.StatusFound) 138 } 139 auth.WriteUserRoleToHttpHeader(req, role) 140 httpCtx := auth.WriteUserRoleToGrpcContext(req.Context(), role) 141 req = req.WithContext(httpCtx) 142 httpHandler(w, req) 143 }