I hit a wall trying to use a custom SSO in my apps so I figured I'd take a step back and ask questions here.
I implemented a RESTful Authentication API in .Net 5.0 (without Identity) by following this article.
I can call http://localhost:4001/api/auth/login with an email and a password to get a JWT in response
{
"token": "eyJhb<--removed jwt-->",
"errors": null
}
Is this a good approach? I didn't want to use something as bulky as Identity or as complicated as or OAuth.
How do I tie this up with a normal authentication "flow"? I know that you have to Challenge the identity provider while logging in, but I couldn't find any exemples on how to do it that would fit my situation. (Itried to implement a custom scheme with builder.AddJwtBearer("my_scheme", ... but this is where I hit a wall)
I managed to do what I wanted, so if anyone stumbles upon this:
Turns out that I was making this out to be super complicated when it's actually quite simple...
The point was to make things easy, so trying to create en authentication scheme was off topic.
Here is the final code:
using var client = new HttpClient() { BaseAddress = new Uri("http://localhost:4001/api/") };
try
{
var stringContent = new StringContent(JsonSerializer.Serialize(new { email = model.Email, password = model.Password }), Encoding.UTF8, "application/json");
var result = await client.PostAsync("auth/login", stringContent);
var jsonResponse = await result.Content.ReadAsStringAsync();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var authResult = JsonSerializer.Deserialize<AuthResult>(jsonResponse, options);
var jwt = authResult.Token.Jwt;
var isValidJwt = JwtHelper.ValidateCurrentToken(jwt, _myExternalAuthSettings.RsaPub, _myExternalAuthSettings.Issuer);
if (isValidJwt)
{
HttpContext.Response.Cookies.Append("my_auth", jwt, new CookieOptions()
{
Domain = "localhost",
Expires = DateTimeOffset.FromUnixTimeSeconds(authResult.Token.UnixTimeExpiresAt)
});
// finish authenticating the user
}
// something failed
}
Note: I followed this article to be able to locally validate the token with the public key (the private key is used to create it).
Cheers
Related
I have a .net core web application and I just want to access some files that are on one of our SharePoint sites, using Microsoft Graph. I've looked at courses on Pluralsight and the most current course has outdated material. I'm looking for a simple code example that gets me from a - z and I can't find any information that exists before mid 2022! On a similar question, I got an answer with code that didn't even work. Apparently I have to get an authorization code, in order to get an access token. BUT, the authorization code pretty much expires as soon as the user is logged into my application. Below is a modified version of the code I was given. I modified it in an effort to try to make it work. As you will see, I tried various version of "scopes" and I'm getting a token that I'm trying to use in the AuthorizationCode Credentials. I don't know if it's the right token to use. I've also seen some examples using PostMan. Getting things to work in postman is absolutely wonderful, but it's not C# code. I apologize if I seem a little rough, I'm just extremally frustrated. It should not be this difficult to find a working code sample. Any help would be appreciated. Here is the code I have that doesn't work:
//var scopes = new[] { "https://mysite.sharepoint.com/.default" };
//var scopes = new[] { "https://graph.microsoft.com/.default" };
var scopes = new[] { "https://graph.microsoft.com/User.ReadWrite.All" };
var tenantId = "tenant";
var clientId = "clientId";
var clientSecret = "shhItsASecret";
var client = new RestClient("https://login.microsoftonline.com/siteId/oauth2/v2.0/token");
var request = new RestRequest();
request.Method = Method.Post;
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddParameter("client_id", clientId);
request.AddParameter("client_secret", clientSecret);
request.AddParameter("scope", "https://graph.microsoft.com/User.ReadWrite.All");
request.AddParameter("response_type", "code");
request.AddParameter("grant_type", "client_credentials");
RestResponse response = client.Execute(request);
TokenModel tokenModel = new TokenModel();
JsonConvert.PopulateObject(response.Content, tokenModel);
var authorizationCode = tokenModel.access_token;
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
// https://learn.microsoft.com/dotnet/api/azure.identity.authorizationcodecredential
var authCodeCredential = new AuthorizationCodeCredential(tenantId, clientId, clientSecret, authorizationCode, options);
Azure.Core.AccessToken accessToken = new Azure.Core.AccessToken();
try
{
accessToken = await authCodeCredential.GetTokenAsync(new Azure.Core.TokenRequestContext(scopes) { });
}
catch (System.Exception ex)
{
throw;
}
var tok = accessToken;
UPDATE:
I now know that I need to use delegated permissions and I need to use the auth code flow in order to do that. However, we use 2 factor authentication and it seems that by the time I can read anything from a variable, I can only see an access-token. If I understand correctly, the auth code is used to get an access-token and it expires. So, I can't seem to use that. Could I pass that access-token to my code that instantiates the graphService?
Someone else suggested I need to adjust my startup file and my appsettings file. I can't really do that. We have 5 other modules in our web application and this would be a big change to all of that. So, I'm not sure what I should be doing there. Bellow is what is in our startup, as it pertains to authentication:
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<UnitRateContractSystemContext>()
.AddDefaultTokenProviders()
.AddUserStore<UserStore<ApplicationUser, ApplicationRole, UnitRateContractSystemContext, Guid, ApplicationUserClaim<Guid>, ApplicationUserRole, IdentityUserLogin<Guid>, IdentityUserToken<Guid>, IdentityRoleClaim<Guid>>>()
.AddRoleStore<RoleStore<ApplicationRole, UnitRateContractSystemContext, Guid, ApplicationUserRole, IdentityRoleClaim<Guid>>>();
UPDATE 3:
I looked a little further down in my startup file and there was some openID connect information. Not sure why it was moved so far down, but I moved it up. Below is my entire authentication setup. The last 4 lines I added as a result of following one of the examples that someone provided. It builds just fine, but when I run it, I get an error in the Program.cs file: System.InvalidOperationException: 'Scheme already exists: Cookies'. If I go and comment out the "AddCookie()" line I get a similar error, but it says that OpenId Connect exists. So, at this point I'm stuck, but I feel if this can be solved, it might be the solution.
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<UnitRateContractSystemContext>()
.AddDefaultTokenProviders()
.AddUserStore<UserStore<ApplicationUser, ApplicationRole, UnitRateContractSystemContext, Guid, ApplicationUserClaim<Guid>, ApplicationUserRole, IdentityUserLogin<Guid>, IdentityUserToken<Guid>, IdentityRoleClaim<Guid>>>()
.AddRoleStore<RoleStore<ApplicationRole, UnitRateContractSystemContext, Guid, ApplicationUserRole, IdentityRoleClaim<Guid>>>();
#region Authentication
string[] initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
//auth
services.AddAuthentication(options =>
{
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = Configuration["Authentication:Microsoft:OAuth"];
options.RequireHttpsMetadata = true;
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.UsePkce = false;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("email");
options.SaveTokens = true;
options.CallbackPath = new PathString(Configuration["Authentication:Microsoft:Callback"]);
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
};
// MetadataAddress represents the Active Directory instance used to authenticate users.
options.MetadataAddress = Configuration["Authentication:Microsoft:Meta"];
options.ClientId = Configuration["Authentication:Microsoft:ApplicationId"];
options.ClientSecret = Configuration["Authentication:Microsoft:Password"];
})
.AddMicrosoftIdentityWebApp(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
You have an asp.net core web application, and you want to access some files that are on one of your SharePoint sites. So I think you may want to use this graph api with request /sites/{site-id}/drive/items/{item-id}. If you want to use other APIs, the steps are the same.
First, since the scenario for you is access files in different sites, so if you used delegated permission(require users sign in first and get access token on behalf the user), you may meet an issue that the user is not allowed to this site so that he can't access the site. I'm afraid this is what you want, so you can use application permissions. For this api, the permission is like below, please add api permissions in Azure AD first.
Then, since you have an asp.net core web application, then you can use Azure identity + graph SDK to do this. You can use code below:
using Microsoft.Graph;
using Azure.Identity;
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "tenant_name.onmicrosoft.com";
var clientId = "aad_app_id";
var clientSecret = "client_secret";
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
var file = await graphClient.Sites["site_id"].Drive.Items["item_id"].Request().GetAsync();
If you want to let user sign in and then list some files which is allowed to the signed in user, the easiest way is adding Microsoft identity platform into your application, which can created by a template, just need to choose the authentication option when creating application in visual studio, then update the configurations. And certainly, you need to give delegated API permission, which is different than above.
Finally here's the official sample, you can see what codes/packages/configurations are added based on a web application.
I am trying to authorize an ASP.NET Core 6 MVC web app to Google analytics data API.
[GoogleScopedAuthorize("https://www.googleapis.com/auth/analytics.readonly")]
public async Task<IActionResult> Index([FromServices] IGoogleAuthProvider auth)
{
var cred = await auth.GetCredentialAsync();
var client = await BetaAnalyticsDataClient.CreateAsync(CancellationToken.None);
var request = new RunReportRequest
{
Property = "properties/" + XXXXX,
Dimensions = {new Dimension {Name = "date"},},
Metrics = {new Metric {Name = "totalUsers"},new Metric {Name = "newUsers"}},
DateRanges = {new DateRange {StartDate = "2021-04-01", EndDate = "today"},},
};
var response = await client.RunReportAsync(request);
}
The authorization goes though as would be expected; I am getting an access token back.
I cant seem to figure out how to apply the credentials to the BetaAnalyticsDataClient.
When I run it without applying it to the BetaAnalyticsDataClient, I get the following error:
InvalidOperationException: The Application Default Credentials are not available. They are available if running in Google Compute Engine. Otherwise, the environment variable GOOGLE_APPLICATION_CREDENTIALS must be defined pointing to a file defining the credentials. See https://developers.google.com/accounts/docs/application-default-credentials for more information.
I am not currently using GOOGLE_APPLICATION_CREDENTIALS as it is configured in programs.cs. I don't see the need to have client id and secret configured in program.cs plus having an added env var.
Why isn't it just picking up the authorization already supplied with the controller runs?
builder.Services
.AddAuthentication(o =>
{
// This forces challenge results to be handled by Google OpenID Handler, so there's no
// need to add an AccountController that emits challenges for Login.
o.DefaultChallengeScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
// This forces forbid results to be handled by Google OpenID Handler, which checks if
// extra scopes are required and does automatic incremental auth.
o.DefaultForbidScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
// Default scheme that will handle everything else.
// Once a user is authenticated, the OAuth2 token info is stored in cookies.
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogleOpenIdConnect(options =>
{
options.ClientId = builder.Configuration["Google:ClientId"];
options.ClientSecret = builder.Configuration["Google:ClientSecret"];
});
Is there an alternate method for authorizing with a web app that I have not been able to find. I did do some dinging in the source code I can't seem to find a method to apply this.
After quite a bit of digging i managed to find that it was possible to create my own client builder and apply the credentials there.
var clientBuilder = new BetaAnalyticsDataClientBuilder()
{
Credential = await auth.GetCredentialAsync()
};
var client = await clientBuilder.BuildAsync();
Hope this helps someone else.
I read in IDS4's tutorial that the following is a way to obtain a token.
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret");
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");
if (tokenResponse.IsError) { ... }
Console.WriteLine(tokenResponse.Json);
However, it's indicated in by the intellisense that it's about to be declared as obsolete so I checked out the docs for identity model password grant type suggesting the following.
var response = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
{
Address = "https://localhost:44300/connect/token",
ClientId = "spa",
ClientSecret = "spa_secret",
Scope = "MemberApi",
UserName = user,
Password = pass
});
I'm not certain what to do next in terms of producing a token. In the object I'm getting, there are AccessToken, IdentityToken, RefreshToken etc. and I'm confused as to which to rely on as the documentation on the differences between those is vague.
As the call goes now, I get an error saying that the client is unauthorized and the tokens are null. The client is declared as follows.
new Client
{
ClientId = "spa",
ClientSecrets = { new Secret("spa_secret".Sha256()) },
ClientName = "SPA client",
ClientUri = "http://identityserver.io",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RedirectUris = { "http://localhost:5000/security/credentials" },
PostLogoutRedirectUris = { "http://localhost:5000/index.html" },
AllowedCorsOrigins = { "http://localhost:5000", "https://localhost:44300" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"MemberApi",
"MemberApi.full",
"MemberApi.limited"
}
}
Instead of using TokenClient, the intention is to use extension methods (on HttpClient) in the future.
The extension method with similar functionality to the TokenClient class is RequestClientCredentialsTokenAsync
Use it as follows:
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("https://auth.myserver.com:5011");
var tokenResponse = await client.RequestClientCredentialsTokenAsync(
new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "MyAppClient",
ClientSecret = "secret",
Scope = "api1"
});
It's confusing, as documentation needs to cover a lot of different situations. It's best you concentrate on the implicit flow, as that's a good candidate for a SPA application.
There is a sample and documentation available from identityServer, take a look at JavaScriptClient.
Next best thing to do is read the specs. That's the source. IdentityServer implements those specifications. In addition you can read the documentation of IdentityServer.
That should help you to understand when and where to use the tokens. It depends on the type of client and the chosen flow. You can find information about that here.
In short the meaning of the tokens:
Access Token: a JWT or reference token, used to access a resource. This token is also used to retrieve information from the UserInfo endpoint.
Identity Token: contains at least the sub claim, in order to identify the user.
Refresh Token: a token to allow a client to obtain a new access token on behalf of the user without requiring interaction with the user (offline access). Only available in certain flows.
Depending on the flow the response can differ.
For documentation about how to access a resource (api) with javascript using the access token please read this. When the access token expires you'll need a new token, without having to ask the user to login again. Since the refresh token isn't supported for the implicit flow you can implement a silent token renew as described here.
About the obsolete code, TokenClient is replaced in favor of HttpClient. Instead of setting up a TokenClient you can now call the extension for the HttpClient. This is a code improvement and doesn't affect the implementation of the flows.
I'm currently confused about how to realize the authentication / authorization flow.
I'm developing two applications, the one is the frontend/Webapplication and the other the backend/API, both with ASP.NET Core. The goal is to use the AzureAD and use the users/groups from the domain. The authentication I already implemented on both applications and I'm able to login and restrict content based on the login state.
As reference I took this example from a microsoft developer. There should be exactly this what I want to do. There is a WebApp and API. The used authentication flow is the authorization code flow. First the user needs to login and after that when some data needs to be requested from the API, an access token will be requested.
Question 1: Is this the right authentication flow? For me this seems like a doubled authentication, because first I authenticate myself at the frontend and when the Webapp needs some data I need to authenticate myself again at the backend. The same Azure AD tenant is used, so what do you think here?
The next point what seems very "ugly" is the procedure getting some data. In the example when some data is requested first the token will be requested and after this the data. But in my opinion with a lot of boilerplate. The example code below is needed for just one request of all ToDo items.
// GET: /<controller>/
public async Task<IActionResult> Index()
{
AuthenticationResult result = null;
List<TodoItem> itemList = new List<TodoItem>();
try
{
// Because we signed-in already in the WebApp, the userObjectId is know
string userObjectID = (User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
// Using ADAL.Net, get a bearer token to access the TodoListService
AuthenticationContext authContext = new AuthenticationContext(AzureAdOptions.Settings.Authority, new NaiveSessionCache(userObjectID, HttpContext.Session));
ClientCredential credential = new ClientCredential(AzureAdOptions.Settings.ClientId, AzureAdOptions.Settings.ClientSecret);
result = await authContext.AcquireTokenSilentAsync(AzureAdOptions.Settings.TodoListResourceId, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
// Retrieve the user's To Do List.
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, AzureAdOptions.Settings.TodoListBaseAddress + "/api/todolist");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.SendAsync(request);
// Return the To Do List in the view.
if (response.IsSuccessStatusCode)
{
List<Dictionary<String, String>> responseElements = new List<Dictionary<String, String>>();
JsonSerializerSettings settings = new JsonSerializerSettings();
String responseString = await response.Content.ReadAsStringAsync();
responseElements = JsonConvert.DeserializeObject<List<Dictionary<String, String>>>(responseString, settings);
foreach (Dictionary<String, String> responseElement in responseElements)
{
TodoItem newItem = new TodoItem();
newItem.Title = responseElement["title"];
newItem.Owner = responseElement["owner"];
itemList.Add(newItem);
}
return View(itemList);
}
//
// If the call failed with access denied, then drop the current access token from the cache,
// and show the user an error indicating they might need to sign-in again.
//
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
return ProcessUnauthorized(itemList, authContext);
}
}
catch (Exception)
{
if (HttpContext.Request.Query["reauth"] == "True")
{
//
// Send an OpenID Connect sign-in request to get a new set of tokens.
// If the user still has a valid session with Azure AD, they will not be prompted for their credentials.
// The OpenID Connect middleware will return to this controller after the sign-in response has been handled.
//
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme);
}
//
// The user needs to re-authorize. Show them a message to that effect.
//
TodoItem newItem = new TodoItem();
newItem.Title = "(Sign-in required to view to do list.)";
itemList.Add(newItem);
ViewBag.ErrorMessage = "AuthorizationRequired";
return View(itemList);
}
//
// If the call failed for any other reason, show the user an error.
//
return View("Error");
}
Question 2: Is there a "less ugly" approach to access the data if the flow in Q1 is right?
I found a proper solution to solve this.
I just used the approach from this example here multitenant-saas-guidance and it works like charm.
I have an application set up with Azure ACS and .net 4.5 using claims. My application uses dropbox also. I was wondering if i could let users identify them self with dropbox alone.
I get a token from dropbox when the user logs in with dropbox and a unique id. Where in the .net pipe do i tell it that i have authenticated a user, such the principals are set on the next request also.
To make the example simple, lets say i have a form with two inputs. name,pass. If the name is 1234 and pass is 1234. then i would like to tell the asp.net pipeline that the user is authenticated. Is this possible? or do i need to create custom token handlers an such to integrate it into WIF?
Update
I found this: I would like comments on the solution, if there are security concerns i should be aware off.
var sam = FederatedAuthentication.SessionAuthenticationModule;
if (sam != null)
{
var cp = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim> {new Claim("Provider","Dropbox")}, "OAuth"));
var transformer = FederatedAuthentication.FederationConfiguration.IdentityConfiguration.ClaimsAuthenticationManager;
if (transformer != null)
{
cp = transformer.Authenticate(String.Empty, cp);
}
var token = new SessionSecurityToken(cp);
sam.WriteSessionTokenToCookie(token);
}
All code:
public HttpResponseMessage get_reply_from_dropbox(string reply_from)
{
var response = this.Request.CreateResponse(HttpStatusCode.Redirect);
var q = this.Request.GetQueryNameValuePairs();
var uid = q.FirstOrDefault(k => k.Key == "uid");
if (!string.IsNullOrEmpty(uid.Value))
{
var sam = FederatedAuthentication.SessionAuthenticationModule;
if (sam != null)
{
var cp = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim> {new Claim("Provider","Dropbox")}, "OAuth"));
var transformer = FederatedAuthentication.FederationConfiguration.IdentityConfiguration.ClaimsAuthenticationManager;
if (transformer != null)
{
cp = transformer.Authenticate(String.Empty, cp);
}
var token = new SessionSecurityToken(cp);
sam.WriteSessionTokenToCookie(token);
}
}
response.Headers.Location = new Uri(reply_from);
return response;
}
public async Task<string> get_request_token_url(string reply_to)
{
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("OAuth",
string.Format("oauth_version=\"1.0\", oauth_signature_method=\"PLAINTEXT\", oauth_consumer_key=\"{0}\", oauth_signature=\"{1}&\"",
"<dropboxkey>","<dropboxsecret>"));
var data = await client.GetStringAsync("https://api.dropbox.com/1/oauth/request_token");
var pars = data.Split('&').ToDictionary(k=>k.Substring(0,k.IndexOf('=')),v=>v.Substring(v.IndexOf('=')+1));
return "https://www.dropbox.com/1/oauth/authorize?oauth_token=" + pars["oauth_token"]
+ "&oauth_callback=<MYSITE>/api/dropbox/get_reply_from_dropbox?reply_from=" + reply_to;
}
It works by the user request the authentication url, when the user authenticates my app it returns to get_reply_from_dropbox and logs in the user.
I offcause needs to handle some other stuff also, like what if the request do not come from dropbox.
I did this for my site using WIF 3.5 (not exactly the same) but it did use ACS+forms auth+OAuth all together, basically it uses form auth (which you can control completely) or use ACS/OAuth and link the accounts together or just use ACS/OAuth by itself.
You will have to handle logging off differently though.
http://garvincasimir.wordpress.com/2012/04/05/tutorial-mvc-application-using-azure-acs-and-forms-authentication-part-1/
DropBox uses OAuth, so I would go that route and then if you want to "link the accounts" create a user/password for forms auth linked to the DropBox Oauth account. The user doesn't necessarily have to know what auth conventions are being used. ASP.NET MVC 4 has the OAuth/forms auth built in the default project.