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  }