Issue
I am trying to use a yubikey for second factor authentication via their OTP Validation Protocol Version 2.0. Despite following all the documentation meticulously (https://developers.yubico.com/OTP/OTP_Walk-Through.html and https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html), I either get a BAD_SIGNATURE or MISSING_PARAMETER status response. I was trying to attach an HMAC-SHA-1 signature only because of the MISSING_PARAMETER response (the signature is not required). When I do try to attach a HMAC-SHA-1 signature then I get the BAD_SIGNATURE status response.
Environment
I am using Visual Studio 2019 and ASP.NET via a custom login form for authentication. Because of the deadline I am attempting to use SharePoint 2019 as a platform since it meets all the requirements OOTB except the 2FA requirement. Since there were issues in the past with AD based authentication I am using Forms auth with ASP.NET Sql Membership as a provider.
Login Code Process
The custom login form first checks the membership provider without setting any auth cookies. If the username and password are valid it proceeds to check if the user requires 2FA via yubikey. If they do I am using an HttpWebRequest to send the get and then reading the response from it (for debugging I am currently printing the response on a label):
string getUrl = "https://api2.yubico.com/wsapi/2.0/verify?id="+YUBICOID+"&otp="+otp+"&nonce="+nonce;
HttpWebRequest yubiGet = (HttpWebRequest)WebRequest.Create(getUrl);
HttpWebResponse response = (HttpWebResponse)yubiGet.GetResponse();
Stream respstr = response.GetResponseStream();
StringBuilder sb = new StringBuilder();
string temp = null;
int ct = 0;
byte[] buffer = new byte[8192];
do
{
ct = respstr.Read(buffer, 0, buffer.Length);
if (ct != 0)
{
temp = Encoding.ASCII.GetString(buffer, 0, ct);
sb.Append(temp);
}
}
while (ct > 0);
string responseStr = sb.ToString();
ErrorLabel.Text = "get: "+getUrl+ "<br /><br />response: " + responseStr;
Per the documentation (see the validation protocol link above) the HMAC signature is not required, however when I leave it off I get the MISSING_PARAMETER response. To build the signature I followed the documentation and reviewed the code in the old YubicoClient class (deprecated). I used the same function calls and processes to generate the signature.
If the username and the creds were verified (via membership provider) and 2FA (yubikey) passed then I set the auth cookie and redirect appropriately.
What I have tried
Everything works except the yubikey response part. I have tried including all of the parameters indicated in the validation protocol documentation, both with and without the signature. With the signature I receive a BAD_SIGNATURE response and without it I receive a MISSING_PARAMETER response.
I am using the Client ID from the Yubico API Key signup from https://upgrade.yubico.com/getapikey/ site for the id url parameter. When I tried with the signature I used the secret key that was generated there. I followed the same process to generate the signature that is used in the YubicoClient class (deprecated)
A separate page is used to link the users to the key and is outside the scope of this issue.
I did use the yubikey manager application to reset the slot and re-registered it with the api key signup multiple times, using both slot 1 and slot 2 on the key.
It is a Yubikey 4 C FIPS. If it works we will be getting a lot of yubikey 5's. The documentation does not differentiate key generations and the yubikey manager indicates that the OTP is supported and there are no issues generating the otp.
Any guidance is greatly appreciated!
Ok. Figured it out. Reduced nonce characters from 40 to 20 and it worked. The documentation stated "from 16 to 40 character long string" I was using a random number generator to get bytes and then converting to string. Not sure if it was the url length or if the generated nonce resulted in more than 40 characters, but when I reduced it from 40 to 20 bytes it worked. I am happy now!
Related
I've been having this issue for a while now. I'm trying to add a Sign in through Steam button, which upon login, not only retrieves the user's ID, but also validates the signature. Steam uses OpenID 2.0.
I have followed the documentation here. I have followed these steps carefully, spending the better part of my day on trying to figure this out. My code is this:
let s = data['openid.signed'].split(',');
let x = Buffer.from(s.map(x => `${x}:${data['openid.' + x]}`).join('\n') + '\n', 'utf8');
let c = crypto.createHash('sha1').update(x).digest('base64');
console.log(x.toString('utf8')); // This is the key:value string
console.log(c); // This is the final result; the generated signature
Where data is the response given from the OpenID provider.
Logging x (key:value pair string) gives the expected output of:
signed:signed,op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle
op_endpoint:https://steamcommunity.com/openid/login
claimed_id:https://steamcommunity.com/openid/id/765611981[1234567]
identity:https://steamcommunity.com/openid/id/765611981[1234567]
return_to:http://127.0.0.1:8000/resolve
response_nonce:2018-12-01T17:53: [some_hash]=
assoc_handle:1234567890
However, my generated hash c does not match the given signature, openid.sig. Note that I use a \n at the end of the above key:value pair string, as that is how I interpreted the documentation.
Note. The reason why I need authentication is that I want to connect the Steam account to an account on my website, and being logged in via Steam gives you full access to your account on my website, meaning that it's of utter importance that a user cannot simply enter another users id and get access to their account (replay attack). Because of this, I need to somehow validate the signature.
I have never worked with OpenID before, so please excuse any foolish mistakes of mine. I highly recommend reading the documentation that is linked above, so that you can verify what I am doing is right.
Kinds regards,
Initial Request
Make your Steam login button link to
https://steamcommunity.com/openid/login?openid.ns=http://specs.openid.net/auth/2.0&openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.return_to=https://mywebsite.com&openid.realm=https://mywebsite.com&openid.mode=checkid_setup
and replace the openid.return_to and openid.realm query string parameters.
openid.return_to: This is the URL that Steam will redirect to upon successful login with appended query string parameters.
openid.realm The URL Steam will ask the user to trust. It will appear as a message like this when the user is on the Steam login page: Sign into {openid.realm} using your Steam account. Note that {openid.realm} is not affiliated with Steam or Valve.
Handling the response
Upon successful login, Steam will redirect to a URL like
https://mywebsite.com/?openid.ns=http://specs.openid.net/auth/2.0&openid.mode=id_res&openid.op_endpoint=https://steamcommunity.com/openid/login&openid.claimed_id=https://steamcommunity.com/openid/id/76561198002516729&openid.identity=https://steamcommunity.com/openid/id/76561198002516729&openid.return_to=https:/%mywebsite.com&openid.response_nonce=2020-08-27T04:44:16Zs4DPZce8qc+iPCe8JgQKB0BiIDI=&openid.assoc_handle=1234567890&openid.signed=signed,op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle&openid.sig=W0u5DRbtHE1GG0ZKXjerUZDUGmc=
To verify the user, make a call from your backend to https://steamcommunity.com/openid/login copying every query string parameter from that response with one exception: replace &openid.mode=id_res with &openid.mode=check_authentication. So the final call will be to this URL:
https://steamcommunity.com/openid/login?openid.ns=http://specs.openid.net/auth/2.0&openid.mode=check_authentication&openid.op_endpoint=https://steamcommunity.com/openid/login&openid.claimed_id=https://steamcommunity.com/openid/id/76561198002516729&openid.identity=https://steamcommunity.com/openid/id/76561198002516729&openid.return_to=https://mywebsite.com&openid.response_nonce=2020-08-28T04:44:16Zs4DPZce8qc+iPCe8JgQKB0BiIDI=&openid.assoc_handle=1234567890&openid.signed=signed,op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle&openid.sig=W0u5DRbtHE1GG0ZKXjerUZDUGmc=
Steam will return a text/plain response like this:
ns:http://specs.openid.net/auth/2.0
is_valid:true
If true the user is valid, false invalid. Note this call will only return true once and subsequent calls with the same parameters will always return false. From here, you can decide how to maintain the user being logged in (such as creating a unique cookie) and return a redirect response to something like your site's homepage, last page before they clicked the Steam login button, or user detail page, etc...
I'm having trouble trying to send a POST message to an Azure SB Queue using PostMan.
The error I get is 401 40103: Invalid authorization token signature
My issue is generating the SAS as I'm trying to follow various articles and examples but I must be missing/overlooking/not understanding something.
If I describe what I've done, hopefully it'll become obvious where I'm making a mistake.
My Queue URL is https://GTRAzure.servicebus.windows.net/subscriptionpreference
My Policy is Submit
I've chosen an expiry date for December: 1512086400
My string-to-sign is https://gtrazure.servicebus.windows.net/subscriptionpreference\n1512086400 which is then encoded as https%3A%2F%2Fgtrazure.servicebus.windows.net%2Fsubscriptionpreference%5Cn1512086400
I then sign this using the Primary Key I get from the Submit policy. I'm using this to test: https://www.freeformatter.com/hmac-generator.html
This generates a code like 425d5ff8beb8da58e6f97e45462037e25ea56bcb63470f9b28761fa012f61090 using SHA-256 Which I then base-64 encode to get NDI1ZDVmZjhiZWI4ZGE1OGU2Zjk3ZTQ1NDYyMDM3ZTI1ZWE1NmJjYjYzNDcwZjliMjg3NjFmYTAxMmY2MTA5MA==
I then put it all together to get this which I place in the text of the Authorization header
SharedAccessSignature sig=NDI1ZDVmZjhiZWI4ZGE1OGU2Zjk3ZTQ1NDYyMDM3ZTI1ZWE1NmJjYjYzNDcwZjliMjg3NjFmYTAxMmY2MTA5MA==&se=1512086400&skn=Submit=&sr=https%3A%2F%2Fgtrazure.servicebus.windows.net%2Fsubscriptionpreference%5Cn1512086400
I think the string to sign which you are providing is incorrect because \n is not getting treated as new line which generates encoded value as :
https%3A%2F%2Fgtrazure.servicebus.windows.net%2Fsubscriptionpreference%5Cn1512086400
which gives Authorization failure.
But if it is treated as new line, it will give value like this:
https%3A%2F%2Fgtrazure.servicebus.windows.net%2Fsubscriptionpreference%0A1512086400
which will not give error.
The DocuSign documentation goes through an easy to follow authorization flow for code grant. I'm able to get the "code" from the initial GET request to /oath/auth but getting the tokens gives me an error of "invalid_grant" when I try in postman. I've followed the steps and have a request that looks like this using account-d.docusign.com for host:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic MjMwNTQ2YTctOWM1NS00MGFkLThmYmYtYWYyMDVkNTQ5NGFkOjMwODc1NTVlLTBhMWMtNGFhOC1iMzI2LTY4MmM3YmYyNzZlOQ==
grant_type=authorization_code&code=ey2dj3nd.AAAA39djasd3.dkn4449d21d
Two other members of my team have also tried with their developer accounts and all are getting invalid_grant errors. Is this no longer supported or are there common errors associated with this error that we might be able to investigate?
Re-check all of your values.
I was also getting the same invalid_grant response and could not figure out why at first. It turns out that I had a typo in the Content-Type header. I was using application/x-www-form-urlencode instead of application/x-www-form-urlencoded.
You may not be, but if you are submitting the exact Authorization Header as you've posted it here in your question (MjMwNTQ2YTctOWM1NS00MGFkLThmYmYtYWYyMDVkNTQ5NGFkOjMwODc1NTVlLTBhMWMtNGFhOC1iMzI2LTY4MmM3YmYyNzZlOQ==) it will fail with that message.
That is the base64 value for the sample integration key and sample secret key provided in their documentation. If you decode that string with an online base64decoder it will result in 230546a7-9c55-40ad-8fbf-af205d5494ad:3087555e-0a1c-4aa8-b326-682c7bf276e9. This is the same sample integration key and secret in the documentation.
Check the Authorization header you are submitting by encoding your integration key and secret (integrationKey:secret) using this online base64encoder. This will make sure the issue isn't with your base64 encoding of your integration key and secret. Once you have that value make sure your Authorization uses the word Basic before the value you got from this website. (Basic base64stringFromOnlineEncoder)
Check that the code your are submitting in the body of the post is not the sample code from their documentation. ey2dj3nd.AAAA39djasd3.dkn4449d21d is the sample code from their documentation. You may just be using that in your question as a placeholder but if you are submitting any of those values it will return invalid_grant. Make sure that the body of your post does not have any leading or trailing spaces.
Have the correct Content-Type set application/x-www-form-urlencoded
Have the correct Authorization header set Basic base64EncodedIntegrationKey:Secret
Have the correct body using the valid code received from the GET request to /oauth/auth with no leading or trailing spaces, making sure you're not using the values from your question.
If you are still having trouble and you are not doing a user application but are doing a service integration you can use Legacy Authentication to get your oAuth2 token.
Alternative Method using Legacy Authentication for Service Integrations
This method does not use a grant code. You pass in the integration key, username and password into the X-DocuSign-Authentication header in JSON format.
Demo Server: demo.docusign.net
Production Server: www.docusign.net API
Version: v2
POST https://{server}/restapi/{apiVersion}/oauth2/token
Content-Type: application/x-www-form-urlencoded
X-DocuSign-Authentication: {"IntegratorKey":"your_integrator_key","Password":"docusign_account_password","Username":"docusign_account_username"}
grant_type=password&client_id=your_integrator_key&username=docusign_account_username&password=docusign_account_password&scope=api
If you are building a user application that requires the user enter their docusign credentials to generate the token, this alternative will not work for you.
For anyone who is facing this error, I'd like to point out this note in the documentation:
Note: The obtained authorization code is only viable for 2 minutes. If more then two minutes pass between obtaining the authorization code and attempting to exchange it for an access token, the operation will fail.
I was struggling with the same error until I spotted the note and sped up my typing to meet the 2 minutes.
Hope it helps someone else.
In my case the problem was related to having set a wrong value for Content-Type header, namely "application/x-www-form-URIencoded" instead of the correct "application/x-www-form-urlencoded". Note though that in my case the problem was not a "typo" but an excessive trust in DocuSign documentation.
Indeed the wrong Content-Type is, at the time of writing, suggested directly into the documentation page where they describe the Authorization Code Grant workflow, see the image below for the relevant part.
Hopefully they will fix the documentation soon but for the time being be careful not to blindly copy & paste the code from their examples without thinking, as I initially did.
anyone have an idea what is wrong here I am getting a BadRequest with the following
{"error":"invalid_grant","error_description":"unauthorized_client"}
var client = new RestClient(ESIGNURL);
var request = new RestRequest("/oauth/token");
request.Method = Method.POST;
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddHeader("Authorization", "Basic " + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(integrationkey+ ":" + secret)));
string body = "grant_type=authorization_code&code=" + code;
request.Parameters.Clear();
request.AddParameter("application/x-www-form-urlencoded", body, ParameterType.RequestBody);
var response = client.Execute(request);
I was getting this error as well. What I realized is I was appending the state at the end of the code before passing it to the oauth token endpoint.
This snippet is from Docusign explaining what are some other reasons for getting that error.
Invalid-error explanation
I just spent a day doing this (in NodeJS). I'll add a couple of things to the answers from before. First, I had to put:
"Content-Type": "application/x-www-form-urlencoded"
in the header. Otherwise it gave me the message:
{
"error": "invalid_grant",
"error_description": "unsupported_grant_type"
}
Second, the base64 encoding:
I used this in NodeJS and it worked
const integration_key = process.env.INTEGRATION_KEY;
const secret_key = process.env.SECRET_KEY;
const authinfo =
integration_key.toString("utf8") + ":" + secret_key.toString("utf8");
const buff2 = Buffer(authinfo, "utf8").toString("base64");
If you use "base64url" it dosen't work because it strips the == off of the end of the string. The = symbol is used as padding and apparently it's needed. You see a similar difference on this site https://www.base64encode.org/ when you toggle the url safe encoding option. If you don't have the padding on the end of your base64 encoded string (or if it's generally incorrect) you get this message:
{
"error": "invalid_grant",
"error_description": "unauthorized_client"
}
Finally, if you're using Postman (I'm using DocuSign's Postman Collection) remember to reset and save the codeFromUrl variable after you update it. Otherwise it doesn't update and you get the message:
{
"error": "invalid_grant",
"error_description": "expired_client_token"
}
This means the old URL code has expired and your new one didn't save.
I'm generating a token on our auth server (Node.js) in node-jsonwebtoken that will be passed to an API (PHP Laravel) and verified by tymondesigns/jwt-auth.
A token generated by tymondesigns/jwt-auth will be verified successfully by
its own verify function, node-jsonwebtoken and jwt.io.
A token generated by node-jsonwebtoken will be verified successfully by its own verify function, jwt.io, but not tymondesigns/jwt-auth.
On the Laravel server, i get the following error when I try to verify a token generated by node-jsonwebtoken:
TokenInvalidException in NamshiAdapter.php line 71:
Token Signature could not be verified.
The payloads look identical when I look at them over at jwt.io. I have even tried to generate the exact same token on the Node server by passing the same iat,sub,iss,exp,nbf and jti as generated by a working token, but tymondesigns/jwt-auth still won't accept it.
Is there anything else that could be causing this, but isn't visible in the decoded information? I'm also not 100% sure how jti works. Maybe there is something preventing this from working about that?
node-jsonwebtoken (7.1.9), tymon/jwt-auth (0.5.9), namshi/jose (5.0.2)
The last version of the namshi/jose library is 7.0.
There is also a known bugs for all ESxxx algorithms.
If you cannot verify signatures using that library, you could try with another one.
I developed a library that supports all features described in the RFCs related to the JWT, including encryption support.
The reason is, as mentioned by Spomky aswell, a bug in namshi/jose related to the iss claim. It is resolved in 7.0 which is used by tymon/jwt-auth 1.0.0-alpha.2. However, since there currently isn't a documented way to install 1.0.0-alpha.2, we probably have to wait for a stable release.
Until then, since the problem and the bug is related to the iss claim, removing the iss requirement from required_claims and generating the tokens without it solves the problem temporarily.
In my case I had a url inside the payload. PHP escapes slashes by default when encoding to JSON, while Node.js doesn't. When the verification JWT gets generated in PHP (with those extra backslashes) of course the final hashes won't match since the payload is just different. Solution is to use the JSON_UNESCAPED_SLASHES flag when converting to JSON inside your JWT library, I was using https://github.com/namshi/jose so I created a simple class like this one:
use Namshi\JOSE\SimpleJWS;
class SimpleJWSWithEncodeOptions extends SimpleJWS
{
protected static $encodeOptions = 0;
public static function setEncodeOptions($options)
{
self::$encodeOptions = $options;
}
/**
* Generates the signed input for the current JWT.
*
* #return string
*/
public function generateSigninInput()
{
$base64payload = $this->encoder->encode(json_encode($this->getPayload(), self::$encodeOptions));
$base64header = $this->encoder->encode(json_encode($this->getHeader(), self::$encodeOptions));
return sprintf("%s.%s", $base64header, $base64payload);
}
}
Then it could be used like:
SimpleJWSWithEncodeOptions::setEncodeOptions(JSON_UNESCAPED_SLASHES);
$jws = SimpleJWSWithEncodeOptions::load($token);
$jws->verify($key);
$data = $jws->getPayload();
This problem was very specific to my payload content but it could help someone
I am creating an ASP.NET MVC5 action method that implements a password reset endpoint and accepts a click-through from an email message containing a token. My implementation uses OWIN middleware and closely resembles the ASP.NET Identity 2.1 samples application.
As per the samples application, the token is generated by UserManager and embedded into a URL that is sent to the user by email:
var token = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
var encoded = HttpServerUtility.UrlTokenEncode(Encoding.UTF8.GetBytes(token));
var uri = new Uri(Url.Link("ResetPasswordRoute", new { id = user.Id, token = encoded }));
The link in the email message targets an MVC endpoint that accepts the token parameter as one of its route segments:
[Route("reset-password/{id}/{token}"]
public async Task<ActionResult> PasswordResetAsync(int id, string token)
{
token = Encoding.UTF8.GetString(HttpServerUtility.UrlTokenDecode(token));
// Implementation here
}
However, requests to this endpoint (using a URL generated in the above manner) fail with Bad Request - Invalid URL.
It appears that this failure occurs because the URL is too long. Specifically, if I truncate the token segment, it connects correctly to the MVC endpoint (although, of course, the token parameter is no longer valid). Specifically, the following truncated URL works ...
http://localhost:53717/account/reset-password/5/QVFBQUFOQ01uZDhCRmRFUmpIb0F3RS9DbCtzQkFBQUFzcko5MEJnYWlrR1RydnVoY2ZwNEpnQUFBQUFDQUFBQUFBQVFaZ0FBQUFFQUFDQUFBQUNVeGZZMzd4OTQ3cE03WWxCakIwRTl4NkVSem1Za2ZUc1JxR2pwYnJSbmJ3QUFBQUFPZ0FBQUFBSUFBQ0FBQUFEcEpnVXFXS0dyM2ZPL2dQcWR1K2x6SkgxN25UVjdMYlE2UCtVRG4rcXBjU0FBQUFE
... but it will fail if one additional character is added ...
http://localhost:53717/account/reset-password/5/QVFBQUFOQ01uZDhCRmRFUmpIb0F3RS9DbCtzQkFBQUFzcko5MEJnYWlrR1RydnVoY2ZwNEpnQUFBQUFDQUFBQUFBQVFaZ0FBQUFFQUFDQUFBQUNVeGZZMzd4OTQ3cE03WWxCakIwRTl4NkVSem1Za2ZUc1JxR2pwYnJSbmJ3QUFBQUFPZ0FBQUFBSUFBQ0FBQUFEcEpnVXFXS0dyM2ZPL2dQcWR1K2x6SkgxN25UVjdMYlE2UCtVRG4rcXBjU0FBQUFEf
I believe that the default IIS configuration setting for maxUrlLength should be compatible with what I am trying to do, but I have also tried explicitly setting it to a larger value, which did not solve the problem.
However, using Fiddler to examine the server response, I can see that the working URL generates a server response with the following header ...
Server: Microsoft-IIS/8.0
... whereas the longer URL is rejected with a response containing the following header ...
Server: Microsoft-HTTPAPI/2.0
This seems to imply that the URL is not being being rejected by IIS, but by a middleware component.
So, I am wondering what that component might be and how I might work around its effect.
Any suggestions please?
Many thanks,
Tim
Note: Although my implementation above Base64 encodes the token before using it in the URL, I have also experimented with the simpler approach used in the sample code, which relies on the URL encoding provided by UrlHelper.RouteUrl. Both techniques suffer from the same issue.
You should not be passing such long values in the application path of the URL as they are limited in length to something like 255 characters.
A slightly better alternative is to use a query string parameter instead:
http://localhost:53717/account/reset-password/5?token=QVFBQUFOQ01uZDhCRmRFUmpIb0F3RS9DbCtzQkFBQUFzcko5MEJnYWlrR1RydnVoY2ZwNEpnQUFBQUFDQUFBQUFBQVFaZ0FBQUFFQUFDQUFBQUNVeGZZMzd4OTQ3cE03WWxCakIwRTl4NkVSem1Za2ZUc1JxR2pwYnJSbmJ3QUFBQUFPZ0FBQUFBSUFBQ0FBQUFEcEpnVXFXS0dyM2ZPL2dQcWR1K2x6SkgxN25UVjdMYlE2UCtVRG4rcXBjU0FBQUFEf
That should be safe for at least 2000 characters (full URL) depending on the browser and IIS settings.
A more secure and scalable approach is to pass a token inside an HTTP header.