Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8286674
feat: implement mTLS Proof of Possession (RFC 8705)
Robbie-Microsoft Apr 8, 2026
f155d38
fix(mtls-pop): bug fixes for Path 2 + attestation DLL integration
Robbie-Microsoft Apr 8, 2026
f32bad4
docs(mtls-pop): fix error messages, add attestation step, Trusted Lau…
Robbie-Microsoft Apr 8, 2026
4771229
fix(mtls-pop): use Software KSP + VBS Virtual Isolation flags for Key…
Robbie-Microsoft Apr 8, 2026
3aa63c5
security: remove private key + PFX from git; add to .gitignore
Robbie-Microsoft Apr 8, 2026
c93bfde
managedidentity: MSAL.NET key provider parity - USER scope + 3-level …
Robbie-Microsoft Apr 8, 2026
c39c0b5
Fix CSR generation to match MSAL.NET Csr.Generate() exactly
Robbie-Microsoft Apr 8, 2026
f5544f8
Switch Path 2 test resource to graph.microsoft.com
Robbie-Microsoft Apr 9, 2026
12fdf2d
Fix cngSigner.Sign to support RSA-PSS padding for CSR generation
Robbie-Microsoft Apr 9, 2026
6c46c76
Fix mTLS PoP token cache: use MtlsPopAuthenticationScheme for cache k…
Robbie-Microsoft Apr 9, 2026
10091cd
docs: update mTLS PoP docs for accuracy post-implementation
Robbie-Microsoft Apr 9, 2026
ab2d8de
test: replace hardcoded tenant GUID with env var in path1 error cases
Robbie-Microsoft Apr 9, 2026
4a62eaa
refactor: extract mTLS setup into helpers to reduce cognitive complexity
Robbie-Microsoft Apr 9, 2026
d451298
fix: guard against integer overflow in CSR allocation (CodeQL)
Robbie-Microsoft Apr 9, 2026
c6ec2ae
refactor: deduplicate JWT decode helper across test drivers
Robbie-Microsoft Apr 9, 2026
f0f1243
refactor: eliminate remaining code duplication flagged by SonarCloud
Robbie-Microsoft Apr 9, 2026
2f0ea83
feat: expose BindingTLSCertificate on AuthResult for Path 2 downstrea…
Robbie-Microsoft Apr 9, 2026
15e4cf0
docs: update downstream call examples and Path 2 expected results
Robbie-Microsoft Apr 9, 2026
98ae986
fix: address SonarCloud findings (constants, complexity, SHA1 comment)
Robbie-Microsoft Apr 9, 2026
6e7e8c2
fix: address SonarCloud round 3 findings
Robbie-Microsoft Apr 9, 2026
06ba7b0
fix: suppress SonarCloud security hotspots for IMDS hardcoded IP
Robbie-Microsoft Apr 9, 2026
0842f66
docs: add mTLS PoP deep-dive architecture documentation
Robbie-Microsoft Apr 9, 2026
4300c58
docs: move regional mTLS endpoint info under Path 1; fix DLL note
Robbie-Microsoft Apr 9, 2026
d52da8b
docs: explain why AttestationClientLib.dll is not bundled in msal-go
Robbie-Microsoft Apr 9, 2026
2f1449a
docs: simplify DLL bundling explanation — purely a Go module limitation
Robbie-Microsoft Apr 9, 2026
925ee85
docs: explain DLL bundling limitation and how to obtain AttestationCl…
Robbie-Microsoft Apr 9, 2026
d00c07b
docs: move architecture diagram to architecture doc as Mermaid, remov…
Robbie-Microsoft Apr 9, 2026
af31687
Update cross-SDK comparison table: add msal-java row, rename section
Robbie-Microsoft Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,11 @@ github.com/
golang.org/
.vscode/
.idea/

# mTLS PoP test credentials — private key and PFX must not be committed.
# test-cert.pem (public cert only) is safe to commit.
# Regenerate with:
# openssl req -x509 -newkey rsa:2048 -keyout apps/tests/devapps/mtls-pop/test-key.pem \
# -out apps/tests/devapps/mtls-pop/test-cert.pem -days 365 -nodes -subj "/CN=msal-go-mtls-test"
apps/tests/devapps/mtls-pop/test-key.pem
apps/tests/devapps/mtls-pop/test-mtls.pfx
92 changes: 87 additions & 5 deletions apps/confidential/confidential.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,9 @@ func AutoDetectRegion() string {
// package doc. A new Client should be created PER SERVICE USER.
// For more information, visit https://docs.microsoft.com/azure/active-directory/develop/msal-client-applications
type Client struct {
base base.Client
cred *accesstokens.Credential
base base.Client
cred *accesstokens.Credential
sendCertOverMtls bool
}

// clientOptions are optional settings for New(). These options are set using various functions
Expand All @@ -256,6 +257,7 @@ type clientOptions struct {
authority, azureRegion string
capabilities []string
disableInstanceDiscovery, sendX5C bool
sendCertOverMtls bool
httpClient ops.HTTPClient
}

Expand Down Expand Up @@ -318,6 +320,17 @@ func WithAzureRegion(val string) Option {
}
}

// WithSendCertificateOverMtls enables bearer-over-mTLS authentication.
// When set, the client certificate authenticates via the TLS handshake instead of
// a JWT client_assertion in the request body. The token type remains Bearer.
// Requires the client to be configured with a certificate credential (NewCredFromCert).
// Mirrors MSAL.NET's WithSendCertificateOverMtls().
func WithSendCertificateOverMtls() Option {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have two PRs - one for SNI and one for MSI

return func(o *clientOptions) {
o.sendCertOverMtls = true
}
}

// New is the constructor for Client. authority is the URL of a token authority such as "https://login.microsoftonline.com/<your tenant>".
// If the Client will connect directly to AD FS, use "adfs" for the tenant. clientID is the application's client ID (also called its
// "application ID").
Expand Down Expand Up @@ -354,7 +367,7 @@ func New(authority, clientID string, cred Credential, options ...Option) (Client
}
base.AuthParams.IsConfidentialClient = true

return Client{base: base, cred: internalCred}, nil
return Client{base: base, cred: internalCred, sendCertOverMtls: opts.sendCertOverMtls}, nil
}

// authCodeURLOptions contains options for AuthCodeURL
Expand Down Expand Up @@ -748,6 +761,7 @@ type acquireTokenByCredentialOptions struct {
authnScheme AuthenticationScheme
extraBodyParameters map[string]string
cacheKeyComponents map[string]string
isMtlsPopRequested bool
}

// AcquireByCredentialOption is implemented by options for AcquireTokenByCredential
Expand All @@ -757,7 +771,7 @@ type AcquireByCredentialOption interface {

// AcquireTokenByCredential acquires a security token from the authority, using the client credentials grant.
//
// Options: [WithClaims], [WithTenantID], [WithFMIPath], [WithAttribute]
// Options: [WithClaims], [WithTenantID], [WithFMIPath], [WithAttribute], [WithMtlsProofOfPossession]
func (cca Client) AcquireTokenByCredential(ctx context.Context, scopes []string, opts ...AcquireByCredentialOption) (AuthResult, error) {
o := acquireTokenByCredentialOptions{}
err := options.ApplyOptions(&o, opts)
Expand All @@ -776,6 +790,12 @@ func (cca Client) AcquireTokenByCredential(ctx context.Context, scopes []string,
}
authParams.ExtraBodyParameters = o.extraBodyParameters
authParams.CacheKeyComponents = o.cacheKeyComponents

// Configure mTLS transport if requested via WithMtlsProofOfPossession or WithSendCertificateOverMtls.
if err := cca.applyMtlsParams(&authParams, o.isMtlsPopRequested); err != nil {
return AuthResult{}, err
}

if o.claims == "" {
silentParameters := base.AcquireTokenSilentParameters{
Scopes: scopes,
Expand All @@ -784,7 +804,7 @@ func (cca Client) AcquireTokenByCredential(ctx context.Context, scopes []string,
Credential: cca.cred,
IsAppCache: true,
TenantID: o.tenantID,
AuthnScheme: o.authnScheme,
AuthnScheme: authParams.AuthnScheme,
Claims: o.claims,
ExtraBodyParameters: o.extraBodyParameters,
CacheKeyComponents: o.cacheKeyComponents,
Expand All @@ -804,6 +824,40 @@ func (cca Client) AcquireTokenByCredential(ctx context.Context, scopes []string,
return cca.base.AuthResultFromToken(ctx, authParams, token)
}

// applyMtlsParams configures authParams for mTLS when WithMtlsProofOfPossession or
// WithSendCertificateOverMtls is requested. Extracted to reduce cognitive complexity
// of AcquireTokenByCredential.
func (cca Client) applyMtlsParams(authParams *authority.AuthParams, isMtlsPopRequested bool) error {
if !isMtlsPopRequested && !cca.sendCertOverMtls {
return nil
}
if cca.cred.Cert == nil {
return errors.New("mTLS requires a certificate credential; use NewCredFromCert")
}
if isMtlsPopRequested {
if err := validateMtlsPopAuthority(authParams); err != nil {
return err
}
authParams.AuthnScheme = authority.NewMtlsPopAuthenticationScheme(cca.cred.Cert)
}
authParams.UseMtlsTransport = true
authParams.MtlsBindingCert = cca.cred.Cert
return nil
}

// validateMtlsPopAuthority checks that the authority is suitable for mTLS PoP
// (must be tenanted and must have a region configured).
func validateMtlsPopAuthority(authParams *authority.AuthParams) error {
tenant := authParams.AuthorityInfo.Tenant
if tenant == "common" || tenant == "organizations" || tenant == "consumers" {
return errors.New("mTLS PoP requires a tenanted authority; use a specific tenant ID in the authority URL")
}
if authParams.AuthorityInfo.Region == "" && authParams.AuthorityInfo.AuthorityType != authority.DSTS {
return errors.New("mTLS PoP requires an Azure region; use WithAzureRegion() or AutoDetectRegion()")
}
return nil
}

// acquireTokenOnBehalfOfOptions contains optional configuration for AcquireTokenOnBehalfOf
type acquireTokenOnBehalfOfOptions struct {
claims, tenantID string
Expand Down Expand Up @@ -902,3 +956,31 @@ func WithAttribute(attrValue string) interface {
),
}
}

// WithMtlsProofOfPossession requests an mTLS Proof of Possession access token (RFC 8705) instead of
// a standard Bearer token. The client must be configured with a certificate credential (NewCredFromCert)
// and an Azure region (WithAzureRegion or AutoDetectRegion). The authority URL must be tenanted
// (not /common or /organizations). On success, AuthResult.BindingCertificate is set to the
// certificate bound to the token; use it as the client certificate in downstream mTLS calls
// with the "Authorization: mtls_pop <token>" header. Mirrors MSAL.NET's WithProofOfPossession().
func WithMtlsProofOfPossession() interface {
AcquireByCredentialOption
options.CallOption
} {
return struct {
AcquireByCredentialOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *acquireTokenByCredentialOptions:
t.isMtlsPopRequested = true
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
Loading
Loading