-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[API Proposal]: Expose a high level authentication API #69920
Comments
Tagging subscribers to this area: @dotnet/ncl, @vcsjones Issue DetailsBackground and motivationruntime already have very similar implementation - used primarily by Tha main goal is to expose existing functionality via public API so other components can leverage it as well. API Proposalnamespace System.Net.Security
{
public class NegotiateAuthenticationClientOptions
{
// Specifies the GSSAPI authentication package used for the authentication.
// Common values are Negotiate, NTLM or Kerberos. Default value is Negotiate.
public string Package { get; set; }
// The NetworkCredential that is used to establish the identity of the client.
// Default value is CrendentialCache.DefaultCredential.
public NetworkCredential Credential { get; set; }
// The Service Principal Name (SPN) that uniquely identifies the server to authenticate.
public string? TargetName { get; set; }
// The ChannelBinding that is used for extended protection.
public ChannelBinding? Binding { get; set; }
// Indicates the requires level of protection of the authentication exchange
// and any further data exchange.
// Valid values: None, Sign, EncryptAndSign
// Default value: None
System.Net.Security.ProtectionLevel RequiredProtectionLevel { get; set; }
}
public class NegotiateAuthenticationServerOptions
{
// Specifies the GSSAPI authentication package used for the authentication.
// Common values are Negotiate, NTLM or Kerberos. Default value is Negotiate.
public string Package { get; set; }
// The NetworkCredential that is used to establish the identity of the client.
// Default value is CrendentialCache.DefaultCredential.
// Note: I'm not quite sure of the meaning of this on the server side but
// it exists on GSSAPI side.
public NetworkCredential Credential { get; set; }
// The ChannelBinding that is used for extended protection.
public ChannelBinding? Binding { get; set; }
// Indicates the requires level of protection of the authentication exchange
// and any further data exchange.
// Valid values: None, Sign, EncryptAndSign
// Default value: None
System.Net.Security.ProtectionLevel RequiredProtectionLevel { get; set; }
}
public class NegotiateAuthentication : IDisposable
{
// Create client-side authentication
public NegotiateAuthentication(NegotiateAuthenticationClientOptions clientOptions);
// Create server-side authentication
public NegotiateAuthentication(NegotiateAuthenticationServerOptions serverOptions);
// Indicates whether the initial authentication finished.
// NOTE: Original it was named IsCompleted in the proposal but I renamed
// it to match the property on NegotiateStream.
public bool IsAuthenticated { get; set; }
// Indicates negotiated protection level (can be higher than the required one).
// Returns None if authentication was not finished yet.
public System.Net.Security.ProtectionLevel ProtectionLevel { get; set; }
// Indicates whether signing was negotiated.
// Returns false if authentication was not finished yet.
public bool IsSigned { get; set; }
// Indicates whether signing was negotiated.
// Returns false if authentication was not finished yet.
public bool IsEncrypted { get; set; }
// Indicates whether the client and server are mutually authenticated.
// Returns false if authentication was not finished yet.
public bool IsMutuallyAuthenticated { get; set; }
// Indicates whether this is server-side authentication context.
// Returns value based on the constructor used, provided for parity with
// NegoatiateStream.
public bool IsServer { get; set; }
// The GSSAPI package name that was used for the authentiation. Initially it's
// the name specified in the options in constructor. After successful authentication
// it should return the actual mechanism used. For example, if Negotiate is
// specified as the requested GSSAPI package this may return NTLM or Kerberos once
// authentication exchange is complete (eg. IsAuthenticated == true).
//
// NOTE: This is partly duplicate with RemoteIdentity so it may not be necessary
// on the public API.
public string Package { get; }
// For server context it returns the SPN target name of the client after successful
// authentication. For client context it returns the target name specified in the
// constructor options.
public string? TargetName { get; }
// Gets information about the identity of the remote party.
//
// When accessed by the client, this property returns a GenericIdentity containing
// the Service Principal Name (SPN) of the server and the authentication protocol used.
//
// Throws InvalidOperationException if authentication was not finished yet
// (IsAuthenticated == false) for server-side.
public System.Security.Principal.IIdentity RemoteIdentity { get };
public byte[] GetOutgoingBlob(ReadOnlySpan<byte> incomingBlob, out NegotiateAuthenticationStatusCode statusCode);
// Base64 version of GetOutgoingBlob
public string GetOutgoingBlob(string incomingBlob, out NegotiateAuthenticationStatusCode statusCode);
// TODO (APIs not necessary for HTTP authentication but necessary for high-level protocols
// like SASL and NegotiateStream):
// Wrap, Unwrap as replacement for Encrypt, Decrypt, MakeSignature and VerifySignature
// GetMIC and VerifyMIC if we find a use for that
}
// NOTE: Mirrors SecurityStatusPalErrorCode at the moment but it should mostly map to GSSAPI error
// codes.
public enum NegotiateAuthenticationStatusCode
{
NotSet = 0,
OK,
ContinueNeeded,
CompleteNeeded,
CompleteAndContinue,
ContextExpired,
CredentialsNeeded,
Renegotiate,
TryAgain,
// Errors
OutOfMemory,
InvalidHandle,
Unsupported,
TargetUnknown,
InternalError,
PackageNotFound,
NotOwner,
CannotInstall,
InvalidToken,
CannotPack,
QopNotSupported,
NoImpersonation,
LogonDenied,
UnknownCredentials,
NoCredentials,
MessageAltered,
OutOfSequence,
NoAuthenticatingAuthority,
IncompleteMessage,
IncompleteCredentials,
BufferNotEnough,
WrongPrincipal,
TimeSkew,
UntrustedRoot,
IllegalMessage,
CertUnknown,
CertExpired,
DecryptFailure,
AlgorithmMismatch,
SecurityQosFailed,
SmartcardLogonRequired,
UnsupportedPreauth,
BadBinding,
DowngradeDetected,
ApplicationProtocolMismatch,
NoRenegotiation
}
} API Usagevar auth = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions("NTLM", testCredential "HTTP/foo"));
NegotiateAuthenticationStatusCode statusCode;
byte[]? negotiateBlob = ntAuth.GetOutgoingBlob(ReadOnlySpan<byte>.Empty, out NegotiateAuthenticationStatusCode statusCode);
sendBlob(negotiateBlob);
do
{
buffer = receiveBlob();
byte[]? negotiateBlob = ntAuth.GetOutgoingBlob(buffer, out statusCode);
} while (statusCode == NegotiateAuthenticationStatusCode .ContinueNeeded);
if (statusCode == NegotiateAuthenticationStatusCode.OK)
{
....
}
else
{
...
}
### Alternative Designs
_No response_
### Risks
This API currently works well for bot client and server side of HTTP. We really have weak test coverage for SMTP and other protocols.
<table>
<tr>
<th align="left">Author:</th>
<td>wfurt</td>
</tr>
<tr>
<th align="left">Assignees:</th>
<td>wfurt</td>
</tr>
<tr>
<th align="left">Labels:</th>
<td>
`api-suggestion`, `area-System.Net.Security`
</td>
</tr>
<tr>
<th align="left">Milestone:</th>
<td>7.0.0</td>
</tr>
</table>
</details> |
Can we make a non-allocating API as well? |
It's non-trivial, unfortunately. |
There is pattern in SslStream where you can pass in buffer via NegotiateAuthenticationStatusCode GetOutgoingBlob(ReadOnlySpan<byte> incomingBlob,ref byte[]? outgoingBlob); |
I am honestly not sure if it's worth it. Another idea would be to return a simple disposable struct. That way it would be possible to pool the buffer and it would make it possible to clear the buffer contents during |
yah. I'm not sure either. |
Apparently the concept in my proposal above already exists as the public IMemoryOwner<byte> GetOutgoingBlob(ReadOnlySpan<byte> incomingBlob, out NegotiateAuthenticationStatusCode statusCode); and do the pooling. |
I guess I should ask: How often is this API called? |
On each Negotiate/NTLM authenticated connection about 3-4 times for NTLM, less for Kerberos. That is true both for the server side (Kestrel) and client side (HttpClient). The authentication is session based, ie. authenticated connection could be reused for multiple requests to the same server and authentication overhead is present only for the first request. The buffers will likely never be big enough to spill to large object heap and given the usage patterns they are likely to live only on gen 0 GC. |
OK I'm convinced, for now 😄. cc @mconnew |
This will work for WCF and CoreWCF |
What number triggers the LOH? By default on Windows we have a MaxTokenSize of 48000 bytes with a max of 65535 bytes. Customers set this when users have more than a few hundred groups (taps out at 1000), so lots of folks set this. In delegation scenarios there are two tokens involved (targeted service ticket + TGT), so double that max size for what's moving across the wire, and since it's moving across the wire it will be base64 encoded so +40% overhead.
Ninor nit. Negotiate is its own specific package. If you're letting callers pass their own package name then it's not doing negotiate. Suggest
It would be quite nice if this were its own interface, or at least have a mechanism to pass another internal implementation. Ideally |
85000 bytes iirc. This is the non-base64 size for this particular API. |
The naming is difficult and I am open to suggestions. The original internal class was named
The idea was to expose single API to consumers but possibly use some internal interface as implementation. Currently there's two backends - the platform API one (GSSAPI / SSPI) and the managed one (NTLM + Negotiate protocols in managed code, no Kerberos). We may eventually explore integrating the managed Kerberos implementation, or combining the different backends (eg. managed Negotiate and NTLM but Kerberos handled through platform APIs). That is, however, not necessarily focus at the moment. We want to unblock scenarios where the current internal |
Passwords and single sign-on (default credentials). |
SSPI is GSS-like, and when we (the SSPI-owning team 😀) discuss things on the wire we generically refer to it as GSS so not the worst option. We also have historically referred to SSPI/GSS as 'platform authentication mechanisms' since most OS platforms don't support anything beyond these. Could potentially go with
Fair enough.
Yes, my point was moreso that it doesn't support other platform credentials like certificates or keytabs. Alternative option is to extend |
|
namespace System.Net.Security;
public class NegotiateAuthenticationClientOptions
{
public string Package { get; set; }
public NetworkCredential Credential { get; set; }
public string? TargetName { get; set; }
public ChannelBinding? Binding { get; set; }
ProtectionLevel RequiredProtectionLevel { get; set; }
}
public class NegotiateAuthenticationServerOptions
{
public string Package { get; set; }
public NetworkCredential Credential { get; set; }
public ChannelBinding? Binding { get; set; }
ProtectionLevel RequiredProtectionLevel { get; set; }
}
public sealed class NegotiateAuthentication : IDisposable
{
public NegotiateAuthentication(NegotiateAuthenticationClientOptions clientOptions);
public NegotiateAuthentication(NegotiateAuthenticationServerOptions serverOptions);
public bool IsAuthenticated { get; }
public ProtectionLevel ProtectionLevel { get; }
public bool IsSigned { get; }
public bool IsEncrypted { get; }
public bool IsMutuallyAuthenticated { get; }
public bool IsServer { get; }
public string Package { get; }
public string? TargetName { get; }
public IIdentity RemoteIdentity { get };
public byte[] GetOutgoingBlob(ReadOnlySpan<byte> incomingBlob, out NegotiateAuthenticationStatusCode statusCode);
public string GetOutgoingBlob(string incomingBlob, out NegotiateAuthenticationStatusCode statusCode);
}
public enum NegotiateAuthenticationStatusCode
{
NotSet = 0,
OK,
ContinueNeeded,
CompleteNeeded,
CompleteAndContinue,
ContextExpired,
CredentialsNeeded,
Renegotiate,
TryAgain,
OutOfMemory,
InvalidHandle,
Unsupported,
TargetUnknown,
InternalError,
PackageNotFound,
NotOwner,
CannotInstall,
InvalidToken,
CannotPack,
QopNotSupported,
NoImpersonation,
LogonDenied,
UnknownCredentials,
NoCredentials,
MessageAltered,
OutOfSequence,
NoAuthenticatingAuthority,
IncompleteMessage,
IncompleteCredentials,
BufferNotEnough,
WrongPrincipal,
TimeSkew,
UntrustedRoot,
IllegalMessage,
CertUnknown,
CertExpired,
DecryptFailure,
AlgorithmMismatch,
SecurityQosFailed,
SmartCardLogonRequired,
UnsupportedPreauth,
BadBinding,
DowngradeDetected,
ApplicationProtocolMismatch,
NoRenegotiation
} |
So I know this API got approved and I wasn't in the meeting but based on the conversation about the non allocating API and the usage, it seems like GetOutgoingBlob is called per request https://github.com/dotnet/aspnetcore/blob/d02dca7d2285090888fa18bd7789c94504c97191/src/Security/Authentication/Negotiate/src/NegotiateHandler.cs#L130. This means we have an allocation per request right? |
To me it seems the state is kept across requests on the same connection. Subsequent requests should already be in the |
I started working on an implementation here: https://github.com/dotnet/runtime/compare/main...filipnavara:negotiate-api?expand=1 (very rough prototype at the moment) |
Would Wrap/Unwrap be implemented in the future? I use NegotiateStream to do Kerberos authentication for an Apache Kudu Client, and part of the authentication is exchanging Kerberos protected messages. Without those methods I'm stuck on NegotiateStream. |
Each unauthenticated request will have a token payload and each partnered response will also have a token response payload until the handshake is complete (hey -> ack, hey2 -> ack2, etc.). This is usually at most 4 roundtrips, but FYI we have some things coming up in Windows that will push this higher as we move to deprecate and kill NTLM. Once the handshake is complete the session is bound to the TCP connection (or whatever persistence is used -- i.e. whatever holds the security context handle for SSPI or GSS) and then there is nothing else going across the wire auth-wise. This will repeat again once the persistence mechanism has closed the handle (TCP closed, switched to different frontend, etc.). |
Likely yes. The plan was to do minimum viable prototype to cover HTTP authentication first, then follow up from there. |
My understanding was that this API will be called to authenticate a user. Once that is done, the payload is stored by the networking API (say HttpClient) and simply put in a header on subsequent calls. So the allocation would happen once per session, rather than once per request. |
I went through the error codes and culled it. There were lot of unrelated SChannel/TLS error codes. I basically reworked it starting with the GSSAPI specification: public enum NegotiateAuthenticationStatusCode
{
Completed = 0, // GSS_S_COMPLETE
ContinueNeeded, // GSS_S_CONTINUE_NEEDED
GenericFailure, // GSS_S_FAILURE/GSS_S_NO_CONTEXT
BadBinding, // GSS_S_BAD_BINDINGS
Unsupported, // GSS_S_BAD_MECH (Unsupported mechanism)
MessageAltered, // GSS_S_BAD_SIG = GSS_S_BAD_MIC
ContextExpired, // GSS_S_CONTEXT_EXPIRED
CredentialsExpired, // GSS_S_CREDENTIALS_EXPIRED
InvalidCredentials, // GSS_S_DEFECTIVE_CREDENTIAL
InvalidToken, // GSS_S_DEFECTIVE_TOKEN
UnknownCredentials, // GSS_S_NO_CRED
QopNotSupported, // GSS_S_BAD_QOP
OutOfSequence, // GSS_S_DUPLICATE_TOKEN/GSS_S_OLD_TOKEN/GSS_S_UNSEQ_TOKEN/GSS_S_GAP_TOKEN + GSS_E_FAILURE
// GSSAPI statuses not mapped:
//
// GSS_S_BAD_STATUS - API misuse
// GSS_S_UNAVAILABLE - context import/export, not exposed
// GSS_S_DUPLICATE_ELEMENT - API misuse
// GSS_S_NAME_NOT_MN - not exposed
// GSS_S_BAD_NAME - API misuse, we only use known name formats
// GSS_S_BAD_NAMETYPE - API misuse, we only use known name types
// GSS_S_UNAUTHORIZED - not exposed
} with the following mapping from the internal statusCode = securityStatus.ErrorCode switch
{
SecurityStatusPalErrorCode.NotSet => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.OK => NegotiateAuthenticationStatusCode.Completed,
SecurityStatusPalErrorCode.ContinueNeeded => NegotiateAuthenticationStatusCode.ContinueNeeded,
// These code should never be returned and they should be handled internally
SecurityStatusPalErrorCode.CompleteNeeded => NegotiateAuthenticationStatusCode.Completed,
SecurityStatusPalErrorCode.CompAndContinue => NegotiateAuthenticationStatusCode.ContinueNeeded,
SecurityStatusPalErrorCode.ContextExpired => NegotiateAuthenticationStatusCode.ContextExpired,
SecurityStatusPalErrorCode.Unsupported => NegotiateAuthenticationStatusCode.Unsupported,
SecurityStatusPalErrorCode.TargetUnknown => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.InternalError => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.PackageNotFound => NegotiateAuthenticationStatusCode.Unsupported,
SecurityStatusPalErrorCode.NotOwner => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.CannotInstall => NegotiateAuthenticationStatusCode.Unsupported,
SecurityStatusPalErrorCode.InvalidToken => NegotiateAuthenticationStatusCode.InvalidToken,
SecurityStatusPalErrorCode.CannotPack => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.QopNotSupported => NegotiateAuthenticationStatusCode.QopNotSupported,
SecurityStatusPalErrorCode.NoImpersonation => NegotiateAuthenticationStatusCode.UnknownCredentials,
SecurityStatusPalErrorCode.LogonDenied => NegotiateAuthenticationStatusCode.UnknownCredentials,
SecurityStatusPalErrorCode.UnknownCredentials => NegotiateAuthenticationStatusCode.UnknownCredentials,
SecurityStatusPalErrorCode.NoCredentials => NegotiateAuthenticationStatusCode.UnknownCredentials,
SecurityStatusPalErrorCode.MessageAltered => NegotiateAuthenticationStatusCode.MessageAltered,
SecurityStatusPalErrorCode.OutOfSequence => NegotiateAuthenticationStatusCode.OutOfSequence,
SecurityStatusPalErrorCode.NoAuthenticatingAuthority => NegotiateAuthenticationStatusCode.InvalidCredentials,
SecurityStatusPalErrorCode.IncompleteCredentials => NegotiateAuthenticationStatusCode.InvalidCredentials,
SecurityStatusPalErrorCode.IllegalMessage => NegotiateAuthenticationStatusCode.InvalidToken,
SecurityStatusPalErrorCode.CertExpired => NegotiateAuthenticationStatusCode.CredentialsExpired,
SecurityStatusPalErrorCode.SecurityQosFailed => NegotiateAuthenticationStatusCode.QopNotSupported,
SecurityStatusPalErrorCode.SmartcardLogonRequired => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.UnsupportedPreauth => NegotiateAuthenticationStatusCode.Unsupported,
SecurityStatusPalErrorCode.BadBinding => NegotiateAuthenticationStatusCode.BadBinding,
// API misuse or incorrect inputs
SecurityStatusPalErrorCode.OutOfMemory => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.InvalidHandle => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.BufferNotEnough => NegotiateAuthenticationStatusCode.GenericFailure,
// Processing partial inputs is not supported, so this is result of incorrect input
SecurityStatusPalErrorCode.IncompleteMessage => NegotiateAuthenticationStatusCode.InvalidToken,
// TLS related non-error codes => map to generic failure
SecurityStatusPalErrorCode.CredentialsNeeded => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.Renegotiate => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.TryAgain => NegotiateAuthenticationStatusCode.GenericFailure,
// TLS related error codes => map to generic failure
SecurityStatusPalErrorCode.DowngradeDetected => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.ApplicationProtocolMismatch => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.NoRenegotiation => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.CertUnknown => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.DecryptFailure => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.AlgorithmMismatch => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.TimeSkew => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.UntrustedRoot => NegotiateAuthenticationStatusCode.GenericFailure,
SecurityStatusPalErrorCode.WrongPrincipal => NegotiateAuthenticationStatusCode.GenericFailure,
}; |
If we want to minimize allocations we could expose something like the following: public void WriteOutgoingBlob<TWriter>(ReadOnlySpan<byte> incomingBlob, TWriter outgoingBlob, out NegotiateAuthenticationStatusCode statusCode) where TWriter : IBufferWriter<byte>; @davidfowl thoughts? |
Underappreciated suggestion :) I have been playing with the pattern for the Wrap/Unwrap APIs and I really like it. Maybe we can add it later with simplified prototype like this: public NegotiateAuthenticationStatusCode WriteOutgoingBlob(ReadOnlySpan<byte> incomingBlob, IBufferWriter<byte> outgoingBlobWriter); |
Not sure it's worth it TBH. Authentication itself shouldn't happen that often. This would be a bigger concern if the outgoing blob would have to be retrieved on every request, which doesn't seem to be the case. |
Agreed for the authentication itself. I want to run |
You can probably start adding to #62202 @filipnavara. That was intended to cover any use beyond just the basic need. (and Kestrel reflection #29270) |
@wfurt Will do. I'd like to get #70720 done first before moving further. Here's a backlog of my API proposals based on experimenting with the new APIs to support additional scenarios:
|
Background and motivation
runtime already have very similar implementation - used primarily by
SocketsHttphandler
and partially by SMTP.Kestrel currently use this internal API via Reflection (#29270). Additionally, we would like to have similar authentication in Android handler that lives outside of runtime repo.
Tha main goal is to expose existing functionality via public API so other components can leverage it as well.
API Proposal
API Usage
Alternative Designs
No response
Risks
This API currently works well for bot client and server side of HTTP. We really have weak test coverage for SMTP and other protocols.
The text was updated successfully, but these errors were encountered: