I have .net core identity email verification endpoint setup like this:
/api/[controller]/{userId}/{emailVerificationCode}
and I encoded it in registration endpoint with Uri. EscapeDataString (decoding with Uri. UnescapeDataString but that's irrelevant here). So when I get email and I click the link, locally I hit endpoint and can debug it, but after deploying to azure (web app resource group) I get this response:
The resource you are looking for has been removed, had its name changed,
or is temporarily unavailable.
When I shorten code to not contain any special characters (which are now encoded so they are for example %2F, %3D etc) endpoint is hit (but ofc token is invalid).
Any idea what could be the case?
The code that is generated is Base64 encoded, and certain characters in Base64 are not allowed in the path segment of a URL by default, for security reasons, even when URL-encoded. While it's possible to change that, you should not, as the security concerns are valid, and you don't want to expose your app to exploits.
Instead, you can simply let the code be part of the query string. The same vulnerabilities do not exist for the query string portion of a URL, and the characters will be allowed there. Alternatively, you can use a different type of code. The token providers used by Identity for things like email confirmation and password resets can be customized.
Identity includes other token providers for the purposes of two-factor auth that you can switch out with, if you like. These use TOTP-based tokens (the 6-7 digit numbers you see all the time with 2FA). Or, you can create your own custom provider and handle it however you like. To change providers, you simply configure the Tokens member when setting up Identity:
services.AddIdentity<ApplicationUser, IdentityRole>(o =>
{
// other options here like password reqs, etc.
o.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultEmailProvider;
o.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider;
o.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
}
The above will cause those three scenarios to generate tokens via EmailTokenProvider, which is one of the TOTP-based providers builtin.
If you want to use a custom provider, you simply create a class that implements IUserTwoFactorTokenProvider<TUser> and register that:
services.AddIdentity<ApplicationUser, IdentityRole>(o =>
{
...
})
.AddTokenProvider<MyCustomTokenProvider<ApplicationUser>>("MyTokenProviderName");
The string you use as the "name" is what you would use to assign it as a token provider in the previous code above, i.e.:
o.Tokens.PasswordResetTokenProvider = "MyTokenProviderName";
Related
I have a React SPA which uses msal. I have configured Azure AD as Identity Provider for my AADB2C. I can signIn/signOut and other operations.
If my user signs out off my application, I want to also sign out of my Identity Provider. I looked a bit into this matter 1, 2, 3, 4, 5.
At this moment, I use msal's logoutRedirect method.
const url = new URL(AadEndSessionEndpoint);
url.searchParams.append('post_logout_redirect_uri', SPAUrl);
instance.logoutRedirect({
postLogoutRedirectUri: url.toString()
});
What happens, after my user signs out of my AADB2C, he gets redirected to the AAD end_session_endpoint. I can sign out there as well, but my user gets stuck there. Even though I'm passing the post_logout_redirect_uri query parameter to go back to my app, it ignores it.
How could I make this work?
You are doing an RP Initiated Logout in OpenID Connect terms, meaning you need to also send the id_token_hint query parameter.
I can also confirm that sending both query string parameters logs out successfully for my Azure developer account:
url.searchParams.append('post_logout_redirect_uri', SPAUrl);
url.searchParams.append('id_token_hint', myIdToken);
I think the MSAL library requires you to use getAccount instead:
const account = msalInstance.getAccount();
await msalInstance.logoutRedirect({
account,
postLogoutRedirectUri: "https://contoso.com/loggedOut"
});
UPDATE
Your code above is not right - the post logout redirect URI should be that of your own app - I expect the library already knows the end session endpoint location - so just do this:
instance.logoutRedirect({
postLogoutRedirectUri: SPAUrl
});
At the same time it is worth being aware that the full standards based GET URL should look like this. With the knowledge of the expected URL you can check that you are sending the right request via browser tools:
https://[AadEndSessionEndpoint]?id_token_hint=[myIdToken]&post_logout_redirect_uri=[SPAUrl]
The end session endpoint should be a value such as this by the way:
https://login.microsoftonline.com/7f071fbc-8bf2-4e61-bb48-dabd8e2f5b5a/oauth2/v2.0/logout
I am attempting to access the SharePoint Online REST API (this is hand coded REST calls, no library being used).
Access tokens are acquired using authorization grant flow as follows:
I send the browser https://login.microsoftonline.com/common/oauth2/authorize?...
This redirects to a handler endpoint that we extract the access code from
I obtain the tenant ID by:
GET https://{tenantname}.sharepoint.com/_vti_bin/client.svc
Then extracting the tenant ID from the WWW-Authenticate header
I then POST https://login.microsoftonline.com/{tenantid}/oauth2/authorize to obtain the access token
When I use that access token, I am able to do queries using:
GET https://{tenantname}.sharepoint.com/_api/search/query?querytext=....
This works and returns documents.
But when I attempt to retrieve information about one of those documents:
GET https://{tenantname}.sharepoint.com/_api/web/getfilebyserverrelativeurl('/TestFiles/test.pdf')
I get a 404 response with the following body:
{"odata.error":{"code":"-2130575338, Microsoft.SharePoint.SPException","message":{"lang":"en-US","value":"The file /TestFiles/test.pdf does not exist."}}}
If I navigate to the URL in a browser (https://{tenantname}.sharepoint.com/TestFiles/test.pdf), it accesses the file without issue.
This makes me think that I'm running into some sort of permission issue.
I have tried setting the following scopes in the authorize redirect:
Attempt 1: scope = Web.Write AllSites.Write Site.Write
Attempt 2: scope = https://{tenantname}.sharepoint.com/.default
Attempt 3: scope = https://{tenantname}.sharepoint.com/Web.Write https://{tenantname}.sharepoint.com/AllSites.Write https://{tenantname}.sharepoint.com/Site.Write
No matter what I set as the scope parameter of the authorize URL, the JWT details of the access token show (I can post the entire decoded JWT if anyone needs it):
"scp": "User.Read"
Nothing I do has any impact on the scp in the token - I have no idea if that's the issue or not. If it is, I would appreciate hearing how to properly request scope.
The application registration in Azure Active Directory has desired permissions (plus more):
What am I doing wrong?
UPDATE: Switching to OAuth endpoint v2.0:
https://login.microsoftonline.com/common/oauth2/v2.0/authorize
With query parameters:
response_type = code
client_id = my app id
redirect_uri = my redirect uri
scope = <varying - I'll explain what happens under different scenarios below>
Here's what I've tried for scopes:
AllSites.Write Site.Write - the redirect has invalid_client with error_description = AADSTS650053: The application '' asked for scope 'AllSites.Write' that doesn't exist on the resource '00000003-0000-0000-c000-000000000000'. Contact the app vendor.
https://{tenantname}.sharepoint.com/AllSites.Write https://.sharepoint.com/Site.Write - the redirect has invalid_client with error description = AADSTS650053: The application '' asked for scope 'Site.Write' that doesn't exist on the resource '00000003-0000-0ff1-ce00-000000000000'. Contact the app vendor.
https://{tenantname}.sharepoint.com/.default - this goes through
But the resulting JWT has only scp=User.Read
The following works: GET https://{tenantname}.sharepoint.com/_api/search/query?querytext=
But the following returns a 404: GET https://{tenantname}.sharepoint.com/_api/web/getfilebyserverrelativeurl('/TestFiles/test.pdf')
I don't understand how Scope=.Default isn't including the allowed permissions from the application registration. And I definitely don't understand why the AllSites.Write scope is failing when it's explicitly specified.
If it helps, I have also tried all of the above using a tenant specific authorize endpoint instead of 'common':
https://login.microsoftonline.com/{tenantid}/oauth2/v2.0/authorize
UPDATE2: More scope changes:
I finally found a magical combination that works:
Use a tenant based URI for the /authorize and /token endpoint and use {tenanturl}\AllSites.Write for the scope (do NOT specify the Site.Write scope):
https://login.microsoftonline.com/{tenantid}/oauth2/v2.0/authorize?response_type=code&client_id={clientid}&redirect_uri={redirecturi}&scope=https%3A%2F%2F{tenantname}.sharepoint.com%2FAllSites.Write
The resulting JWT has the following:
"scp": "AllSites.Write User.Read"
I am completely perplexed about why Site.Write wasn't allowed. I suppose that AllSites.Write is a superset of Site.Write, so maybe not needed?
All of my testing so far has been on my own tenant, next step is to test on a different tenant and make sure it actually works there as well.
It seems you use v1.0 endpoint https://login.microsoftonline.com/common/oauth2/authorize but not v2.0 endpoint https://login.microsoftonline.com/common/oauth2/v2.0/authorize. If we use v1.0 endpoint, we should use resource instead of scope. So that is why the scp claim in your access token always the same no matter you modify the scope.
You should use resource with https://{tenant-name}.sharepoint.com and the parameter scope is useless when you use v1.0 endpoint.
If you still want to use scope parameter, you can also change the endpoint to v2.0. Just add v2.0 into your endpoint, like: https://login.microsoftonline.com/common/oauth2/v2.0/authorize
I finally found a magical combination that works:
use the https://login.microsoftonline.com/{tenantid}/oauth2/v2.0/authorize and https://login.microsoftonline.com/{tenantid}/oauth2/v2.0/token endpoints
specify {tenanturl}\AllSites.Write for the scope (do NOT specify the Site.Write scope - that was the primary problem):
https://login.microsoftonline.com/{tenantid}/oauth2/v2.0/authorize?response_type=code&client_id={clientid}&redirect_uri={redirecturi}&scope=https%3A%2F%2F{tenantname}.sharepoint.com%2FAllSites.Write
The resulting JWT has the following: "scp": "AllSites.Write User.Read"
This works across tenants and gets us the access we need.
For thoroughness, we also specify offline_access scope so we get a refresh_token in addition to the access_token.
Our application allows assigning permission to groups, which means for every user, we have to reliably determine group membership. The user presents a token regularly obtained with ADAL (some use .NET, others use NodeJS, others use CLI).
Some users seem to be sending a token with the following claim:
"hasgroups": true,
That claim is documented in the Azure AD token reference page.
We would like to add a test case for that, but after following steps here and here, we always end up with a token with the following claims:
"_claim_names": {
"groups": "src1"
},
"_claim_sources": {
"src1": {
"endpoint": "https://graph.windows.net/{redacted}/users/{redacted}/getMemberObjects"
}
},
What is wrong with our setup? Why can't we get the hasgroups claim?
Here are some additional information:
Application type is Native (not WebApi).
Manifest says "oauth2AllowImplicitFlow": true.
The application is given access to Azure Key Vault.
We use the following code to get the token (in C#):
var userCredential = new UserCredential( _userName, _password );
result = context.AcquireToken( _resource, _clientId, userCredential );
Where:
_userName and _password are from a user with lots of groups.
_clientId is the application id of the native application - the one with "oauth2AllowImplicitFlow": true.
_resource is https://vault.azure.net.
The token is emitted correctly. The only issue is that it shows _claim_names and _claims_sources instead of hasgroups.
Where: • _userName and _password are from a user with lots of groups.
As the user is part of lots of groups (assuming 6 or more here).. Azure AD token will come back with a groups overage indicator instead of actual group ids in “groups” claim. I guess you know that and hence doing it intentionally.
var userCredential = new UserCredential( _userName, _password );
result = context.AcquireToken( _resource, _clientId, userCredential );
Since you're acquiring the token in a .NET based application using C# code, the token response is not really limited in length (like in cases for a web SPA, where it is being returned as a URI fragment and URL length has limits)
Looking at the documentation both "hasgroups" and "groups:src1" claims have the same intention of telling that there are too many groups to return as part of the token. Although there is a subtle difference:
in cases where URL limit applies, "hasgroups" will be sent as true (like implicit grant flow for SPA)
in cases where length is not limited (like in your case), Azure AD will still not return all the groups to make sure the token doesn't get too big, but it will send a little more information on how to get to all groups by sending the information on how you can query for all your groups. In this case it's sending the "groups:src1" and "_claim_sources" with source information instead of just the "hasgroups"
Claims in id_tokens
For anyone looking more on this. Please refer Doc saml-tokens
Note
Source : Azure Sample Link
We make use of a thrird party Email Service Provider (ESP) to send emails to our customers. One of the emails needs to have a file attached to it. To accomplish this we need to give the ESP a URL of the file. The ESP will download the file and attach it to the email.
We are now working on a solution to secure the URL to limit the risk of unauthorized downloads. The ESP cannot authenticate itself while making the request to the URL, so the only option we see is by making the URL hard to guess and making it valid for a limited time.
The way we want to accomplish this is by putting a token in the URL's querystring. The service that hosts the file will validate the token and authorize access. The token will need to expire in time and should only give access to a specific file.
We already have a IdentityServer 3 implementation running an oAuth2 Security Token Service. Our plan is to let this service generate the tokens that will be put in the query string of the file download URL. We are considering creating a custom oAuth2 grant type to support tokens that will only allow access to a specific resource.
This would be an example of a request to the oauth2 token endpoint with the custom grant:
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=custom_grant&scope=attachment.read&resource_id=xxxxx
The STS will return a token with a claim of the resource ID the token will give access to.
Is the right use case for a custom grant type or should we look for a different way to implement this?
I think what you're proposing is fine, but I personally prefer using something based on cryptographic signatures for this sort of thing.
JSON Web Token is a well-established way to do this with support in a lot of programming languages. Essentially, you sign a JSON dictionary, including an expiration time. Then you attach it to the URL (e.g. as a query parameter). When receiving an inbound request, you validate the token by checking the signature, expiration time, and the resource it says it applies to.
The advantage of this approach is that there's no needless state: you don't have to store tokens anywhere and instead just need a signing key.
Here's an example in Python that shows the basic idea:
import time
import jwt
SIGNING_SECRET = 'use-a-good-secret-here'
token = jwt.encode({
'resource_id': 'xxxxx',
# JSON spec establishes "exp" as meaning expiration time
'exp': time.time() + 1, # one second in the future for testing
}, SIGNING_SECRET, algorithm='HS256')
print(token)
def try_decoding(token):
try:
decoded = jwt.decode(token, SIGNING_SECRET, algorithms=['HS256'])
print('Valid token for "{}"'.format(decoded['resource_id']))
except jwt.exceptions.ExpiredSignatureError:
print('The token has expired.')
try_decoding(token) # prints 'Valid token for "xxxxx"'
print('Sleeping until the token expires...')
time.sleep(2)
try_decoding(token) # prints "The token has expired."
I need to set up a two-step authentication chain with OpenAM. In the first step, the module requests a user certificate (which has to have been previously linked with an userID) and sends it to an external web service that will validate it and return the userID, which becomes the name of the Principal:
public Principal getPrincipal()
{
return new DataStorePrincipal(userID);
}
On the second step, the module asks the user to type in his userID and password. How do I make sure that the userID typed is the same as the one from step 1?
The modules are chained like this:
Certificate - REQUISITE
ID/Password - REQUIRED
The first module could save the 'userId' in the shared-state map, the 2nd module can read it from the shared-state map. You may look at existing auth-modules source as they provide support for 'shared-state'
You may also look at 'http://docs.forgerock.org/en/openam/10.1.0/admin-guide/index.html#configure-authn-chains'