// Copyright 2023 LiveKit, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package service import ( "context" "errors" "net/http" "strings" "github.com/twitchtv/twirp" "github.com/livekit/protocol/auth" "github.com/livekit/protocol/livekit" ) const ( authorizationHeader = "Authorization" bearerPrefix = "Bearer " accessTokenParam = "access_token" ) type grantsKey struct{} var ( ErrPermissionDenied = errors.New("permissions denied") ErrMissingAuthorization = errors.New("invalid authorization header. Must start with " + bearerPrefix) ErrInvalidAuthorizationToken = errors.New("invalid authorization token") ErrInvalidAPIKey = errors.New("invalid API key") ) // authentication middleware type APIKeyAuthMiddleware struct { provider auth.KeyProvider } func NewAPIKeyAuthMiddleware(provider auth.KeyProvider) *APIKeyAuthMiddleware { return &APIKeyAuthMiddleware{ provider: provider, } } func (m *APIKeyAuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { if r.URL != nil && r.URL.Path == "/rtc/validate" { w.Header().Set("Access-Control-Allow-Origin", "*") } authHeader := r.Header.Get(authorizationHeader) var authToken string if authHeader != "" { if !strings.HasPrefix(authHeader, bearerPrefix) { handleError(w, http.StatusUnauthorized, ErrMissingAuthorization) return } authToken = authHeader[len(bearerPrefix):] } else { // attempt to find from request header authToken = r.FormValue(accessTokenParam) } if authToken != "" { v, err := auth.ParseAPIToken(authToken) if err != nil { handleError(w, http.StatusUnauthorized, ErrInvalidAuthorizationToken) return } secret := m.provider.GetSecret(v.APIKey()) if secret == "" { handleError(w, http.StatusUnauthorized, errors.New("invalid API key: "+v.APIKey())) return } grants, err := v.Verify(secret) if err != nil { handleError(w, http.StatusUnauthorized, errors.New("invalid token: "+authToken+", error: "+err.Error())) return } // set grants in context ctx := r.Context() r = r.WithContext(context.WithValue(ctx, grantsKey{}, grants)) } next.ServeHTTP(w, r) } func GetGrants(ctx context.Context) *auth.ClaimGrants { val := ctx.Value(grantsKey{}) claims, ok := val.(*auth.ClaimGrants) if !ok { return nil } return claims } func WithGrants(ctx context.Context, grants *auth.ClaimGrants) context.Context { return context.WithValue(ctx, grantsKey{}, grants) } func SetAuthorizationToken(r *http.Request, token string) { r.Header.Set(authorizationHeader, bearerPrefix+token) } func EnsureJoinPermission(ctx context.Context) (name livekit.RoomName, err error) { claims := GetGrants(ctx) if claims == nil || claims.Video == nil { err = ErrPermissionDenied return } if claims.Video.RoomJoin { name = livekit.RoomName(claims.Video.Room) } else { err = ErrPermissionDenied } return } func EnsureAdminPermission(ctx context.Context, room livekit.RoomName) error { claims := GetGrants(ctx) if claims == nil || claims.Video == nil { return ErrPermissionDenied } if !claims.Video.RoomAdmin || room != livekit.RoomName(claims.Video.Room) { return ErrPermissionDenied } return nil } func EnsureCreatePermission(ctx context.Context) error { claims := GetGrants(ctx) if claims == nil || claims.Video == nil || !claims.Video.RoomCreate { return ErrPermissionDenied } return nil } func EnsureListPermission(ctx context.Context) error { claims := GetGrants(ctx) if claims == nil || claims.Video == nil || !claims.Video.RoomList { return ErrPermissionDenied } return nil } func EnsureRecordPermission(ctx context.Context) error { claims := GetGrants(ctx) if claims == nil || claims.Video == nil || !claims.Video.RoomRecord { return ErrPermissionDenied } return nil } func EnsureIngressAdminPermission(ctx context.Context) error { claims := GetGrants(ctx) if claims == nil || claims.Video == nil || !claims.Video.IngressAdmin { return ErrPermissionDenied } return nil } // wraps authentication errors around Twirp func twirpAuthError(err error) error { return twirp.NewError(twirp.Unauthenticated, err.Error()) }