I’m working on a site where we want to use Kerberos authentication using Spring Security Kerberos. So, we don’t support NTLM. When the user makes an unauthenticated request, the server will reply with an HTTP 401 with header WWW-Authenticate: Negotiate.
The problem:
For some users/configurations, the browser will send NTLM credentials. The server is not necessarily running on Windows so it can’t handle the NTLM credentials.
As I understand, “Negotiate” means “please send me Kerberos if possible, or else send NTLM”. Is there a different setting that says “only send me Kerberos”? Or is there some way to tell the browsers the site only supports Kerberos?
As a follow-up, why would the browser not have Kerberos available? In this case they are logged in to the same domain. Maybe their credentials have expired?
Kerberos and Spnego should not be confused. Though Spnego is often used for Kerberos authentication, Spnego does not always mean Kerberos, or even a preference for Kerberos.
Spnego is a protocol that allows client and server to negotiate a mutually acceptable mech type (if available).
That may or may not be Kerberos depending on the sub-mechanisms requested by the client and server during the negotiation process.
The Negotiation process may take several handshake attempts.
Using human languages as an example. If I speak English, Latin and Zulu, in that order of preference, and you speak Eskimau and Zulu, then we will end up speaking Zulu.
In the setup that I am currently testing, with Internet Explorer as a client, and a custom Java Application Server using JAAS + GSS as the Server I observe similar behavour to that in your comment:
Browser sends an unauthenticated request
Server replies with HTTP 401 Unauthorized, WWW-Authenticate: Negotiate header.
Browser either responds with Negotiate + NTLM token (bad!).
In my case the game does not end there, it continues as follows:
Server replies with HTTP 401 Unauthorized, WWW-Authenticate: Negotiate + GSS response token
Browser responds with Negotiate + Spnego NegoTokenTarg wrapping a Kerberos Token.
Server unwraps the Kerberos Token; decodes, and authenticates the client; responds with HTTP 200, WWW-Authenticate: Negotiate + GSS response token
i.e. I don't prevent the browser sending an NTLM token, my Server just continues negotiation for another round until it gets a Kerberos Token.
As a side issue: the token provided by Internet Explorer 11 at step 3. above is not properly Spnego compliant, it is neither a NegTokenInit, nor a NetTokenTarg, and at 127 bytes long is clearly much too short to be or wrap a Kerberos token.
You are using Spring Security Kerberos, but in a comment you indicate an interest in other libraries, so below is my JGSS based Spnego authentication code.
For brevity I leave out the JAAS setup, but all this takes place in a JAAS Subject.doAs() privileged context.
public static final String NEGOTIATE = "Negotiate ";
public static final String AUTHORIZATION = "Authorization";
public static final String WWWAUTHENTICATE = "WWW-Authenticate";
public static final int HTTP_OK = 200;
public static final int HTTP_GOAWAY = 401; //Unauthorized
public static final String SPNEGOOID = "1.3.6.1.5.5.2";
public static final String KRB5OID = "1.2.840.113554.1.2.2";
public void spnegoAuthenticate(Request req, Response resp, Service http) {
GSSContext gssContext = null;
String kerberosUser = null;
String auth =req.headers("Authorization");
if ( auth != null && auth.startsWith(NEGOTIATE )) {
//smells like an SPNEGO request, so get the token from the http headers
String authBody = auth.substring(NEGOTIATE.length());
int offset =0;
// As GSS cannot directly process Spnego NegTokenInit and NegTokenTarg, preprocess and extract native Kerberos token.
authBody = preProcessToken(authBody);
try {
byte gssapiData[] = Base64.getDecoder().decode(authBody);
gssContext = initGSSContext(SPNEGOOID, KRB5OID);
byte token[] = gssContext.acceptSecContext(gssapiData, offset, gssapiData.length);
if (gssapiData.length > 128) {
//extract the Kerberos User. The Execute/Login service will compare this with the user in the message body.
kerberosUser = gssContext.getSrcName().toString();
resp.status(HTTP_OK);
} else {
//Is too short to be a kerberos token (or to wrap one), so don't try and extract the user.
//This could be a first pass from an SPNEGO enabled Web-browser. Maybe NTLM?
resp.status(HTTP_GOAWAY);
}
String responseToken = Base64.getEncoder().encodeToString(token);
if (responseToken != null && responseToken.length() > 0) {
resp.header(WWWAUTHENTICATE, NEGOTIATE + responseToken);
}
} catch (GSSException e) {
// Something went wrong fishing the token from the http headers
http.halt(401, "Go Away! This is a privileged route, and you ain't privileged!"+"\r\n");
} finally {
try {
gssContext.dispose();
} catch (GSSException e) {
//error handling here
}
}
} else {
//This is either not a SPNEGO request, or is the first pass without token
resp.header(WWWAUTHENTICATE, NEGOTIATE.trim()); //set header to suggest negotiation
http.halt(HTTP_GOAWAY, "Go Away! This is a privileged route, and you ain't privileged! Only come back when you are."+"\r\n");
}
}
private String preProcessToken(String authBody) {
String tag = getTokenType(authBody);
if (tag.equals("60")) {
// is a standard "application constructed" token. Kerberos tokens seem to start with "YI.."
} else if (tag.equals("A0")) {
// is a Spnego NegTokenInit, starting with "oA.." to "oP.."
authBody=extractKerberosToken(authBody);
} else if (tag.equals("A1")) {
// is a Spnego NegTokenTarg, starting with "oQ.." to "oZ.."
authBody=extractKerberosToken(authBody);
} else {
// some other unexpected token.
// TODO: generate error
}
return authBody;
}
private String extractKerberosToken(String authBody) {
return authBody.substring(authBody.indexOf("YI", 2));
}
private String getTokenType(String authBody) {
return String.format("%02X", Base64.getDecoder().decode(authBody.substring(0,2))[0]);
}
Note this code is presented "as-is", as an example. It is work-in-progress and has a number of flaws:
1) getTokenType() uses the decoded token, but extractKerberosToken works on the encoded token, both should use byte operations on the decoded token.
2) Token rejection based on length is a little too simple. I plan to add better NTLM token identification....
3) I don't have a true GSS context loop. If I don't like what the client presents, I reject and close the context.
For any following handshake attempts from the client I open a new GSS context.
Related
I am using the SignalR .net core client in my project with JWT Tokens.
In the sample code below, the string variable "tokenString" has already been configured as an actual token and therefore i don't need to call upon an external method to create the token, that part has already been done before I reach this method. Using debug, and also testing the "tokenString" value on JWT website, I know the token is working, its just the fact I dont know how to use the ready made token in the SignalR connection method.
How do I configure the SignalR client connection to use this tokenString?
localConConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:44372/LocalConnectorHub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(tokenString); // Not working
// Need a solution like this: options.Token = tokenString
})
.WithAutomaticReconnect()
.Build();
The issue was the fact that the [Authorize] attribute I had configured in the SignalR Hub class needed to define the authentication scheme to use, [Authorize] attribute alone was not enough.
SignalR Hub Class:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class LocalConnectorHub : Hub
{
public async Task SendToMacros(string serverName, string data)
{
await Clients.All.SendAsync("MacrosInbound", serverName, data);
}
public async Task ConnectorStatus(string serverName, string data)
{
await Clients.All.SendAsync("UpdateConnectorStatus", serverName, data);
}
}
SignalR .NET Core Client Connection:
localConConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:44372/LocalConnectorHub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(tokenString);
})
.WithAutomaticReconnect()
.Build();
await localConConnection.StartAsync();
Further example code from the startup.cs class (inside configure services method), this is posted to help one of our fellow members in the comments below:
// Retrieve the secret key from the appsettings.json file used for encryption
// when generating the JWT token for REST API authentication.
var key = Encoding.ASCII.GetBytes(Configuration.GetSection("AppSettings:Token").Value);
// Added to original .net core template.
// The preceding code configures multiple authentication methods. The app uses cookie-based authentication to log in
// via the browser using the identity manager. The second methid uses JWT bearer authentication for the REST API.
// The preceding cookie configuration configures Identity with default option values.
// Services are made available to the app through dependency injection.
// Cookie configuration MUST be called after calling AddIdentity or AddDefaultIdentity.
// IMPORTANT NOTE:
// When we decorate controllers or classes with use the [Authorize] attribute, it actually binds to the first authentication
// system by default (in this case cookie authentication) The trick is to change the attribute to specify which authorization
// service we want to use. Anexample for a protected respurce for a REST API controller would be to decorate using:
// "[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]"
services.AddAuthentication()
.AddCookie(options =>
{
// Cookie settings
options.Cookie.Name = "MyCompanyName";
// HttpOnly is a flag that can be used when setting a cookie to block access to the cookie from client side scripts.
// Javascript for example cannot read a cookie that has HttpOnly set. This helps mitigate a large part of XSS attacks
// as many of these attempt to read cookies and send them back to the attacker, possibly leaking sensitive information
// or worst case scenario, allowing the attacker to impersonate the user with login cookies.
options.Cookie.HttpOnly = true;
// CookieAuthenticationOptions.ExpireTimespan is the option that allows you to set how long the issued cookie is valid for.
// The cookie is valid for (XX) minutes from the time of creation. Once those XX minutes are up the user will have to sign
// back in becuase if the SlidingExpiration is set to false.
// If SlidingExpiration is set to true then the cookie would be re-issued on any request half way through the ExpireTimeSpan.
// For example, if the user logged in and then made a second request half way through the permitted timespan then the cookie
// would be re-issued for another (XX) minutes. If the user logged in and then made a second request AFTER (XX) minutes later
// then the user would be prompted to log in.
// You can also change the units i.e. TimeSpan.FromHours(10); OR TimeSpan.FromDays(10);
// In a nutshell, setting the options.ExpireTimeSpan is equivalent to setting an idle time out period...
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.LoginPath = "/Identity/Account/Login";
options.AccessDeniedPath = "/Identity/Account/AccessDenied";
// Sliding expiration resets the expiration time for a valid authentication cookie if a request is made and more than half of the
// timeout interval has elapsed.If the cookie expires, the user must re - authenticate.Setting the SlidingExpiration property to
// false can improve the security of an application by limiting the time for which an authentication cookie is valid, based on the
// configured timeout value.
options.SlidingExpiration = true;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// The "iss" (issuer) claim identifies the principal that issued the JWT. The processing of this
// claim is generally application specific. The "iss" value is a case-sensitive string containing
// a StringOrURI value. Use of this claim is OPTIONAL.
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
// The "iss" (issuer) claim identifies the principal that issued the JWT.The processing of this
// claim is generally application specific. The "iss" value is a case-sensitive string containing
// a StringOrURI value.Use of this claim is OPTIONAL.
ValidateIssuer = false,
// Usually, this is your application base URL
ValidIssuer = "http://localhost:45092/",
// The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal
// intended to process the JWT MUST identify itself with a value in the audience claim. If the principal
// processing the claim does not identify itself with a value in the "aud" claim when this claim is present,
// then the JWT MUST be rejected. In the general case, the "aud" value is an array of case-sensitive strings,
// each containing a StringOrURI value. In the special case when the JWT has one audience, the "aud" value
// MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience
// values is generally application specific. Use of this claim is OPTIONAL.
ValidateAudience = false,
//Here, we are creating and using JWT within the same application.
//In this case, base URL is fine.
//If the JWT is created using a web service, then this would be the consumer URL.
ValidAudience = "http://localhost:45092/",
// The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted
// for processing. The processing of the "exp" claim requires that the current date/time MUST be before the
// expiration date/time listed in the "exp" claim.
RequireExpirationTime = true,
// Check if token is not expired and the signing key of the issuer is valid (ValidateLifetime = true)
ValidateLifetime = true,
};
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
// Sending the access token in the query string is required due to
// a limitation in Browser APIs. We restrict it to only calls to the
// SignalR hub in this code.
// See https://learn.microsoft.com/aspnet/core/signalr/security#access-token-logging
// for more information about security considerations when using
// the query string to transmit the access token.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hubs")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
Appsettings.json file (dont store keys here for production :)
"AppSettings": {
"Token": "secret key for jwt"
}
This fails on SessionAs in the baseservice in Postman when I authenticate via JWT. But when I use Basic Auth it works fine. Anyone know why?
Apphost
Plugins.Add(new AuthFeature(() => new CustomUserSession(),
new IAuthProvider[]
{
new BasicAuthProvider(), //Sign-in with HTTP Basic Auth
new JwtAuthProvider(AppSettings) {
AuthKeyBase64 = AppSettings.GetString("jwt.auth.key"),
RequireSecureConnection = false,
}, //JWT TOKENS
new CredentialsAuthProvider(AppSettings)
})
{
BaseService
public class ServiceBase: Service
{
public IUserAuth UserAuth
{
get
{
var session = SessionAs<AuthUserSession>();
return AuthRepository.GetUserAuth(session.UserAuthId);
}
}
}
Your SessionAs<T> needs to match the UserSession Type registered in the AuthFeature plugin which is CustomUserSession.
ServiceStack's JwtAuthProvider populates the UserAuthId in the JWT's sub JWT Payload so you should check the Raw HTTP Headers to make sure the JWT Token is being sent, either in HTTP's Authorization Header as a BearerToken or in the ss-tok Cookie. If it is being sent you decode the JWT sent in https://jwt.io to make sure it contains a valid payload, in this case it contains a "sub" property in the JWT payload containing the UserAuthId of the user being authenticated.
I'm developing an Android app, which uses my REST backend. The backend is running on an JBoss instance, which is secured through Keycloak. Since I've updated my Keycloak from 1.0.7 to 2.1.5 I'm experiencing the following problem.
If I try to call a REST API of my backend, JBoss writes the folowing log:
[org.keycloak.adapters.BearerTokenRequestAuthenticator] (default task-39)
Failed to verify token: org.keycloak.common.VerificationException: Invalid token issuer.
Expected 'http://localhost:8180/auth/realms/myrealm', but was 'http://192.168.178.24:8180/auth/realms/myrealm'
at org.keycloak.TokenVerifier.verify(TokenVerifier.java:156)
at org.keycloak.RSATokenVerifier.verify(RSATokenVerifier.java:89)
192.168.178.24 is the right IP address. It seems to be a configuration issue, but where can I config this address?
Has anybody an idea how to fix this problem?
Very simple solution: Make sure when any of your component contact with Keycloak server, they use the same url.
Detailed explanations:
For your case (same as mine), it seems that your Android app is making http request to http://192.168.178.24:8180/... while your server is requesting (or at least configured to) http://192.168.178.24:8180/.... So change your server such that it will request http://192.168.178.24:8180/....
P.S. The exception seems to be the expected behavior to avoid some attacks.
If you take a look into the implementation, here it throws your Exception.
public static class RealmUrlCheck implements Predicate<JsonWebToken> {
private static final RealmUrlCheck NULL_INSTANCE = new RealmUrlCheck(null);
private final String realmUrl;
public RealmUrlCheck(String realmUrl) {
this.realmUrl = realmUrl;
}
#Override
public boolean test(JsonWebToken t) throws VerificationException {
if (this.realmUrl == null) {
throw new VerificationException("Realm URL not set");
}
if (! this.realmUrl.equals(t.getIssuer())) {
throw new VerificationException("Invalid token issuer. Expected '" + this.realmUrl + "', but was '" + t.getIssuer() + "'");
}
return true;
}
};
I think your Client configuration is not correct. Do you have the same clients as in your Keycloak?
I have a scenario where a client has an OpenIdConnect (OIDC) token in their possession. The OIDC was issued from an external OIDC provider, I am not the OIDC provider, just the downstream consumer of it.
The goal is for the client to exchange said OIDC Token, for temporary credentials, or an accesstoken, which will then give them api access to more specific resources.
In my case, the OIDC represents a user. The client, has a ClientId/Secret, which is used to establish service-2-service trust. In the end I would like to have something that looks a lot like the CustomGrant token Request.
static TokenResponse GetCustomGrantToken()
{
var client = new TokenClient(
token_endpoint,
"custom_grant_client",
"cd19ac6f-3bfa-4577-9579-da32fd15788a");
var customParams = new Dictionary<string, string>
{
{ "some_custom_parameter", "some_value" }
};
var result = client.RequestCustomGrantAsync("custom", "read", customParams).Result;
return result;
}
where my customParams would contain the OIDC to my user.
Problem: I can get a token back from the GetCustomGrantToken call, however a follow up Webapi call fails to pass Authorization. i.e. Identity.isAuthenticated is false.
The it all works fine if I get a clientcredential token.
static TokenResponse GetClientToken()
{
var client = new TokenClient(
token_endpoint,
"silicon",
"F621F470-9731-4A25-80EF-67A6F7C5F4B8");
return client.RequestClientCredentialsAsync("api1").Result;
}
Had the CustomGrantToken worked I would have put my users account info in the claims, thus giving me context in the subsequent WebApi calls.
Any direction would be appreciated.
I've used the Yammer API extensively for accessing current users internal network. All API calls have been working correctly (GET's and POST's) with the original token extracted from;
"https://www.yammer.com/oauth2/access_token.json?client_id={App ID}&client_secret={App Secret}&code={Access Code}"
and using the headers; "Authorization : Bearer {Token}" and "Cookie : {Cookies Received from HTML request}.
I've gotten the tokens for all accessible networks using;
"https://www.yammer.com/api/v1/oauth/tokens.json".
Accessing external networks beyond this point has proved troublesome. I changed the header to "Authorization : Bearer {NetworkToken}". While I am able to GET details from external networks, I cannot POST to external networks. I always receive a '401 Unauthorized' response. The 'Unauthorized' requests include deleting messages and liking messages in external networks.
Is there another step between being able to read data from an external network and enabling POST methods?
If I could get any insight into this i'd be extremely grateful!
Cheers!
When accessing external networks, you need to set the authToken to the authToken for that external network.
Step 1 - Get all auth tokens:
yam.platform.request({
url: "oauth/tokens.json",
type: 'GET',
success: function (msg) {
accessTokens = msg;
/....
},
error: function (msg) {
console.log(msg);
error(msg);
}
Step 2: Set the authToken to the correct external network
var currentToken = "";
$.each(accessTokens, function (i,val) {
if (val.network_permalink == $.cookie('networkPermalink')) {
currentToken = val;
}
});
While I was working on a project last month, I used the following way to post message.
The message has to be Byte encrypted in UTF-8 format.
Specify the content type as "application/x-www-form-urlencoded".
So, an example code would be:
HttpWebRequest a = (HttpWebRequest)WebRequest.Create(postUrl);
a.Headers.Add("Authorization", "Bearer" + authToken);
a.Method = "POST";
byte[] message = Encoding.UTF8.GetBytes("body=" + message + "&replied_to_id=" + threadID);
a.ContentType = "application/x-www-form-urlencoded";
a.ContentLength = message.Length;
using (var postStream = request.GetRequestStream())
{
postStream.Write(message, 0, message.Length);
}
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
using (var postStreamForResponse = response.GetResponseStream())
{
StreamReader postReader = new StreamReader(postStreamForResponse);
string results = postReader.ReadToEnd();
postReader.Close();
}
I've discovered quite a few inconsistencies quirks with the Yammer API. I've figured out external networks in their totality now. Here are some things that may not be clear;
When doing a POST or DELETE request, do not include the network_permalink in the url! Only include the network_permalink when you're doing a GET request. This was my main issue.
Required request headers;
Content-Type : application/x-www-form-urlencoded
Accept : application/json
Cookie : _workfeed_session_id=(A code that can be extracted from the response from your first request with an auth token)
Authorization : Bearer (Access token for whichever network you wish to access)
Oh and just FYI, to request threads within the 'All Company' group this is the url; https://www.yammer.com/(network_permalink)/api/v1/messages/general.json
Thanks for the answers!