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  }