github.com/google/osv-scalibr@v0.4.1/veles/secrets/azuretoken/detector.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package azuretoken 16 17 import ( 18 "regexp" 19 20 "github.com/google/osv-scalibr/veles" 21 "github.com/google/osv-scalibr/veles/secrets/common/jwt" 22 ) 23 24 // JWT payload claim keys used to identify Azure tokens. 25 // Reference: https://learn.microsoft.com/en-us/entra/identity-platform/access-token-claims-reference#payload-claims 26 const ( 27 // payloadIssuerKey represents the 'iss' (issuer) claim. 28 payloadIssuerKey = "iss" 29 30 // payloadScopeKey represents the 'scp' (scope) claim. 31 payloadScopeKey = "scp" 32 ) 33 34 // detector is a Veles Detector. 35 type detector struct{} 36 37 // NewDetector returns a Detector that extracts and validates Azure tokens. 38 func NewDetector() veles.Detector { 39 return &detector{} 40 } 41 42 func (d *detector) MaxSecretLen() uint32 { 43 return jwt.MaxTokenLength 44 } 45 46 // Detect checks whether a JWT is a valid Azure token. 47 // It verifies that the token has a valid structure and an accepted Azure issuer 48 // ('iss'). 49 // 50 // If the token contains the scope ('scp') claim is then classified as 51 // access token, otherwise as id token. 52 // 53 // References: 54 // - https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols 55 // - https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens 56 // - https://learn.microsoft.com/en-us/entra/identity-platform/id-tokens 57 func (d *detector) Detect(data []byte) (secrets []veles.Secret, positions []int) { 58 tokens, jwtPositions := jwt.ExtractTokens(data) 59 for i, t := range tokens { 60 payloadClaims := t.Payload() 61 62 // Validate Azure issuer. 63 iss, ok := payloadClaims[payloadIssuerKey].(string) 64 if !ok || !isValidAzureIssuer(iss) { 65 continue 66 } 67 68 // Differentiate between access token and id token. 69 _, hasScope := payloadClaims[payloadScopeKey] 70 71 if hasScope { 72 secrets = append(secrets, AzureAccessToken{Token: t.Raw()}) 73 positions = append(positions, jwtPositions[i]) 74 } else { 75 secrets = append(secrets, AzureIdentityToken{Token: t.Raw()}) 76 positions = append(positions, jwtPositions[i]) 77 } 78 } 79 80 return secrets, positions 81 } 82 83 // According to the Azure documentation, access/id tokens can be issued by: 84 // - https://login.microsoftonline.com/{tenant-id}/v2.0 85 // - https://sts.windows.net/{tenant-id}/ 86 // 87 // Both forms include a tenant ID as a 36-character GUID 88 var ( 89 reLoginMicrosoft = regexp.MustCompile(`^https://login\.microsoftonline\.com/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/v2\.0$`) 90 reStsWindows = regexp.MustCompile(`^https://sts\.windows\.net/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/?$`) 91 ) 92 93 // isValidAzureIssuer reports whether the given issuer ('iss') URL matches 94 // one of the valid Azure issuer formats. 95 func isValidAzureIssuer(iss string) bool { 96 return reLoginMicrosoft.MatchString(iss) || reStsWindows.MatchString(iss) 97 }