The ASP.NET Security Social Sample has two ways to interact with Google.
UseOAuthAuthentication
app.UseOAuthAuthentication(new OAuthOptions
{
AuthenticationScheme = "Google-AccessToken",
DisplayName = "Google-AccessToken",
ClientId = Configuration["google:clientid"],
ClientSecret = Configuration["google:clientsecret"],
CallbackPath = new PathString("/signin-google-token"),
AuthorizationEndpoint = GoogleDefaults.AuthorizationEndpoint,
TokenEndpoint = GoogleDefaults.TokenEndpoint,
Scope = { "openid", "profile", "email" },
SaveTokens = true
});
UseGoogleAuthentication
app.UseGoogleAuthentication(new GoogleOptions
{
ClientId = Configuration["google:clientid"],
ClientSecret = Configuration["google:clientsecret"],
SaveTokens = true,
Events = new OAuthEvents()
{
OnRemoteFailure = ctx =>
{
ctx.Response.Redirect("/error?FailureMessage="
+ UrlEncoder.Default.Encode(ctx.Failure.Message));
ctx.HandleResponse();
return Task.FromResult(0);
}
}
});
What is the standard name for these two types of authentication and authorization? I.e. is one OAuth and the other OpenID Connect?
When choosing to UseOAuthAuthentication, this is the result.
context
.User.Claims: []
.User.Identity.Name: null
.Authentication.GetTokenAsync("access_token"): ya29.CjAlAz3AcUnRD...
.Authentication.GetTokenAsync("refresh_token"): null
.Authentication.GetTokenAsync("token_type"): Bearer
.Authentication.GetTokenAsync("expires_at"): 2016-07-19T22:49:54...
When choosing to UseGoogleAuthentication, this is the result.
context
.User.Claims: [
nameidentifier: 10424487944...
givenname: Shaun
surname: Luttin
name: Shaun Luttin
emailaddress: admin#shaunl...
profile: https://plus.google.com/+ShaunLuttin
]
.User.Identity.Name: "Shaun Luttin"
.Authentication.GetTokenAsync("access_token"): ya29.CjAlAz3AcUnRD...
.Authentication.GetTokenAsync("refresh_token"): null
.Authentication.GetTokenAsync("token_type"): Bearer
.Authentication.GetTokenAsync("expires_at"): 2016-07-19T22:49:54...
Both UseOAuthAuthentication and UseGoogleAuthentication are OAuth. The difference is that the Google middleware sets some default OAuth options that are specific to Google and adds a GoogleHandler that gets the user profile information.
In other words,
UseOAuthAuthentication is OAuth that retrieves and access token.
UseGoogleAuthentication is OAuth with its options and flow tuned to retrieve an access code and user profile information from Google.
Related
I have an Angular 9 web application connected via the oidc-client to Identity Server 4 and an API using Implicit flow. When I get the authenticated user I can see several claims I want for the site such as the email address, the user name or the role.
I'm now trying to do the exact same thing using the password flow and I'm getting only the sub claim back - note this is the first time I use it and therefore it may not be right, but in essence, below would be the call I'm performing (using this time angular-oauth2-oidc through my ionic app) - for simplicity and for testing purposes I'm using postman to illustrate this:
I have modified my client to allow the profile scope without any luck and also I'm getting a different type of response and claim processing targetting the same user using the same configuration on IS4:
My question is, is there anything special I need to set up in my client when I use the password flow to get the claims back or do I need to modify the profile service to include them all the time? I would have imagined when you have access to different scopes and they have issued claims you should get them back but I'm not sure if I'm missing something fundamental here.
My client's config:
public static IEnumerable<Client> Get()
{
return new List<Client>
{
new Client
{
ClientId = "web",
ClientName = "Web Client",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
AllowedScopes = new List<string> { "openid", "profile", "myapi" },
RedirectUris = new List<string> {
"http://<base-url>/auth-callback",
"http://<base-url>/silent-renew-callback",
},
PostLogoutRedirectUris = new List<string> {"http://<base-url>"},
AllowedCorsOrigins = new List<string> {"http://<base-url>"},
AllowAccessTokensViaBrowser = true,
RequireConsent = false,
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true,
},
new Client
{
ClientId = "mobile",
ClientName = "Mobile Client",
ClientSecrets = { new Secret("t8Xa)_kM6apyz55#SUv[[Cp".Sha256()) },
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
AllowedScopes = new List<string> { "openid", "mobileapp", "myapi" },
AccessTokenType = AccessTokenType.Jwt,
AccessTokenLifetime = 3600,
IdentityTokenLifetime = 3600,
UpdateAccessTokenClaimsOnRefresh = false,
SlidingRefreshTokenLifetime = 30,
AllowOfflineAccess = true,
RefreshTokenExpiration = TokenExpiration.Absolute,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
AlwaysSendClientClaims = true,
Enabled = true
}
};
}
}
Any tips are highly appreciated. Many thanks!
UPDATE: Since ROPC flow is being deprecated in oauth 2.1 (https://fusionauth.io/blog/2020/04/15/whats-new-in-oauth-2-1) I decided to move everything to the code flow + PKCE mechanism.
Password grant is an OAuth grant and is to obtain an access token. And what you see as a result of password grant is an access token. access token does not contain any information about the user itself besides their ID (sub claim).
But Implicit grant you use is OpenId Grant. You use oidc client lib and use "openid", "profile" on client - AllowedScopes. What you get in result in an id token. This token authenticates the user to the application and contains user info.
Read more about tokens here.
And this is a very good post which Diagrams of All The OpenID Connect Flows
I am trying to implement OAuth to one of my companies' projects and can't resolve the following problem.
We used IdentityServer4 for implementing our own Authorization Server, which works fine so far. The resource I want to protect with OAuth is a WebApi utilizing Swagger/Swashbuckle.
I followed the IdentityServer4 QuickStartExamples to configure the server and this tutorial [Secure Web APIs with Swagger, Swashbuckle, and OAuth2 (part 2)](http://knowyourtoolset.com/2015/08/secure-web-apis-with-swagger-swashbuckle-and-oauth2-part-2 for configuring Swagger/Swashbuckle).
I have a dummy-action which does nothing else than returning a string, that works as expected.
When I decorate the action with [Authorize], a little red icon appears in swagger-ui, indicating that I have to log in to access this method. The Login process works fine: I am redirected to the Quickstart-UI, can login with the testuser "Bob", and I am redirected to swagger-ui after a successful login.
The problem: After the successful login, I still get an 401 error, stating "Authorization has been denied for this request."
I can see that a bearer token is returned by my IdentityServer in swagger-ui, so I guess this part working fine and the problem seems to be swagger/swashbuckle.
Is there maybe anything else I have to do with the token? In the tutorials I read so far, the swagger config is modified as I did it (see below) and that's it, so I guess swagger/swashbuckle should handle this - but maybe I miss out something?
SwaggerConfig.cs:
c.OAuth2("oauth2")
.Description("OAuth2 Implicit Grant")
.Flow("implicit") //also available: password, application (=client credentials?)
.AuthorizationUrl("http://localhost:5000/connect/authorize")
.TokenUrl("http://localhost:5000/connect/token")
.Scopes(scopes =>
{
scopes.Add("My.Web.Api", "THE Api");
});
// etc. .....
c.OperationFilter<AssignOAuth2SecurityRequirements>();
// etc. .....
c.EnableOAuth2Support(
clientId: "swaggerui",
clientSecret: "secret",
realm: "dummyrealm",
appName: "Swagger UI"
);
Filter for Authorize Attribute in SwaggerConfig.cs:
public class AssignOAuth2SecurityRequirements : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
// Determine if the operation has the Authorize attribute
var authorizeAttributes = apiDescription
.ActionDescriptor.GetCustomAttributes<AuthorizeAttribute>();
if (!authorizeAttributes.Any())
return;
// Initialize the operation.security property
if (operation.security == null)
operation.security = new List<IDictionary<string, IEnumerable<string>>>();
// Add the appropriate security definition to the operation
var oAuthRequirements = new Dictionary<string, IEnumerable<string>>
{
{ "oauth2", new [] { "My.Web.Api" } }
};
operation.security.Add(oAuthRequirements);
}
}
IdentityServer api config:
new ApiResource("My.Web.Api", "THE Api")
IdentityServer client config:
new Client
{
ClientId = "swaggerui",
ClientName = "Swagger UI",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
AllowedCorsOrigins = { "http://localhost:5858" },
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5858/swagger/ui/o2c-html" },
PostLogoutRedirectUris = { "http://localhost:5858/swagger/ui/o2c-html" },
AllowedScopes =
{
"My.Web.Api"
}
Screenshot of redirection after login:
When using .NET Core (but it would appear that this question is for .NET Framework) I also encountered this same problem. It was solved by ensuring that in the Configure method of Startup you have UseAuthentication before UseAuthorization
(source https://learn.microsoft.com/en-us/aspnet/core/grpc/authn-and-authz?view=aspnetcore-3.1)
TL;DR
How do you POST data in an ASP.NET MVC project (form, jQuery, axios), using IdentityServer3 as the authentication server. Also, what flow to use, to make this work?
What I'm experiencing
I have a working IdentityServer3 instance. I also have an ASP.NET MVC project. Using hybrid flow, as I will have to pass the user's token to other services. The authentication itself works - when the pages are only using GET. Even if the authenticated user's tokens are expired, something in the background redirects the requests to the auth. server, and the user can continue it's work, without asking the user to log in again. (As far as I understand, the hybrid flow can use refresh tokens, so I assume that's how it can re-authenticate the user. Even if HttpContext.Current.User.Identity.IsAuthenticated=false)
For testing purposes, I set the AccessTokenLifetime, AuthorizationCodeLifetime and IdentityTokenLifetime values to 5 seconds in the auth. server. As far as I know, the refresh token's expire time measured in days, and I did not change the default value.
But when I try to use POST, things get "ugly".
Using form POST, with expired tokens, the request gets redirected to IdentityServer3. It does it's magic (the user gets authenticated) and redirects to my page - as a GET request... I see the response_mode=form_post in the URL, yet the posted payload is gone.
Using axios POST, the request gets redirected to IdentityServer3, but fails with at the pre-flight OPTIONS request.
Using the default jQuery POST, got same error. (Even though, the default jQuery POST uses application/x-www-form-urlencoded to solve the pre-flight issue.)
startup.cs
const string authType = "Cookies";
// resetting Microsoft's default mapper
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
// ensure, that the MVC anti forgery key engine will use our "custom" user id
AntiForgeryConfig.UniqueClaimTypeIdentifier = "sub";
app.UseCookieAuthentication(new Microsoft.Owin.Security.Cookies.CookieAuthenticationOptions
{
AuthenticationType = authType
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
RedirectUri = adminUri,
PostLogoutRedirectUri = adminUri,
Authority = idServerIdentityEndpoint,
SignInAsAuthenticationType = authType,
ResponseType = "code id_token",
Scope = "openid profile roles email offline_access",
Notifications = new OpenIdConnectAuthenticationNotifications
{
#region Handle automatic redirect (on logout)
RedirectToIdentityProvider = async n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType ==
OpenIdConnectRequestType.LogoutRequest)
{
var token = n.OwinContext.Authentication.User.FindFirst(idTokenName);
if (token != null)
{
var idTokenHint =
token.Value;
n.ProtocolMessage.IdTokenHint = idTokenHint;
}
}
},
#endregion
AuthorizationCodeReceived = async n =>
{
System.Diagnostics.Debug.Print("AuthorizationCodeReceived " + n.ProtocolMessage.ToString());
// fetch the identity from authentication response
var identity = n.AuthenticationTicket.Identity;
// exchange the "code" token for access_token, id_token, refresh_token, using the client secret
var requestResponse = await OidcClient.CallTokenEndpointAsync(
new Uri(idServerTokenEndpoint),
new Uri(adminUri),
n.Code,
clientId,
clientSecret
);
// fetch tokens from the exchange response
identity.AddClaims(new []
{
new Claim("access_token", requestResponse.AccessToken),
new Claim("id_token", requestResponse.IdentityToken),
new Claim("refresh_token", requestResponse.RefreshToken)
});
// store the refresh_token in the session, as the user might be logged out, when the authorization attribute is executed
// see OrganicaAuthorize.cs
HttpContext.Current.Session["refresh_token"] = requestResponse.RefreshToken;
// get the userinfo from the openId endpoint
// this actually retreives all the claims, but using the normal access token
var userInfo = await EndpointAndTokenHelper.CallUserInfoEndpoint(idServerUserInfoEndpoint, requestResponse.AccessToken); // todo: userinfo
if (userInfo == null) throw new Exception("Could not retreive user information from identity server.");
#region Extract individual claims
// extract claims we are interested in
var nameClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Name,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Name)); // full name
var givenNameClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.GivenName,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.GivenName)); // given name
var familyNameClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.FamilyName,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.FamilyName)); // family name
var emailClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Email,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Email)); // email
var subClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Subject,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Subject)); // userid
#endregion
#region Extract roles
List<string> roles;
try
{
roles = userInfo.Value<JArray>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Role).Select(r => r.ToString()).ToList();
}
catch (InvalidCastException) // if there is only 1 item
{
roles = new List<string> { userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Role) };
}
#endregion
// attach the claims we just extracted
identity.AddClaims(new[] { nameClaim, givenNameClaim, familyNameClaim, subClaim, emailClaim });
// attach roles
identity.AddClaims(roles.Select(r => new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Role, r.ToString())));
// update the return value of the SecurityTokenValidated method (this method...)
n.AuthenticationTicket = new AuthenticationTicket(
identity,
n.AuthenticationTicket.Properties);
},
AuthenticationFailed = async n =>
{
System.Diagnostics.Debug.Print("AuthenticationFailed " + n.Exception.ToString());
},
MessageReceived = async n =>
{
System.Diagnostics.Debug.Print("MessageReceived " + n.State.ToString());
},
SecurityTokenReceived = async n =>
{
System.Diagnostics.Debug.Print("SecurityTokenReceived " + n.State.ToString());
},
SecurityTokenValidated = async n =>
{
System.Diagnostics.Debug.Print("SecurityTokenValidated " + n.State.ToString());
}
}
});
Have you configured cookie authentication middleware in the MVC app? After the authentication with identity server, an authentication cookie should be set. When the authentication cookie is set and valid IdentityServer redirection will not occur until the cookie expires/deleted.
Update 1:
Ok, I misunderstood the quesion. It is logical to redirect to identity server when session times out. It won't work with post payload. You can try doing something like follows.
If the request is a normal post, redirect user again to the form
fill page.
If request is ajax post, return unauthorized result and based on
that response refresh the page from javascript.
Anyway I don't think you will be able to keep the posted data unless you are designing your own solution for that. (e.g keep data stored locally).
But you might be able to avoid this scenario altogether if you carefuly decide identity server's session timeout and your app's session timeout.
In OpenIdConnectAuthenticationOptions set UseTokenLifetime = false that will break connection between identity token's lifetime and cookie session lifetime.
In CookieAuthenticationOptions make sliding expiration
SlidingExpiration = true,
ExpireTimeSpan = TimeSpan.FromMinutes(50),
Now you are incontrol of your apps session lifetime. Adjust it to match your needs and security conserns.
I am trying to access token URL working with IdentityServer3. The Server is configured the following way:
var options = new IdentityServerOptions
{
LoggingOptions = new LoggingOptions
{
WebApiDiagnosticsIsVerbose = true,
EnableWebApiDiagnostics = true,
EnableHttpLogging = true,
EnableKatanaLogging= true
},
Factory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get())
.UseInMemoryUsers(Users.Get()),
RequireSsl = false,
EnableWelcomePage = false,
};
app.UseIdentityServer(options);
The client configuration:
new Client
{
Enabled = true,
ClientName = "JS Client",
ClientId = "js",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"http://localhost:56522"
},
AllowedCorsOrigins = new List<string>
{
"http://localhost:56522"
},
AllowAccessToAllScopes = true
}
Trying to POST the following HTTP request to token endpoint:
Content-Type:application/x-www-form-urlencoded
grant_type:password
redirect_uri:http://localhost:56522
client_id:js
username:bob
password:secret
scope:api
I get Invalid client error message and log shows:
Action returned 'IdentityServer3.Core.Results.TokenErrorResult'', Operation=ReflectedHttpActionDescriptor.ExecuteAsync
Any ideas what do I still miss?
Your request is using the password grant type, which is the OAuth Resource Owner flow, but your client is configured to use the OpenID Connect Implicit flow.
Either change your client configuration to use the Resource Owner flow, or change your request to be a valid OpenID Connect request.
For example: GET /connect/authorize?client_id=js&scope=openid api&response_type=id_token token&redirect_uri=http://localhost:56522&state=abc&nonce=xyz. This will take you to a login page.
Or better yet, use a JavaScipt library like #Jenan suggested, such as the IdentityModel oidc-client which handles these requests for you.
I am using Postman to test OAuth 2 from a vanilla AEM install.
Postman can successfully obtain the authorization code from /oauth/authorize after I grant access:
But when it tries to use the code to obtain a token from /oauth/token it receives the following response:
HTTP ERROR: 403 Problem accessing /oauth/token. Reason: Forbidden
Powered by Jetty://
Looking in Fiddler it is doing a POST to /oauth/token with the following Name/Values in the body:
client_id: Client ID from /libs/granite/oauth/content/client.html
client_secret:
Client Secret from /libs/granite/oauth/content/client.html
redirect_uri: https://www.getpostman.com/oauth2/callback
grant_type: authorization_code
code: Code returned from previous request to oauth/authorize
Am I missing something?
Would help if you can list some code snippets on how you are building the url and fetching the token.
Here's an example of how we've implemented very similar to what you are trying to do, maybe it'll help.
Define a service like below (snippet) and define the values (host, url, etc) in OSGI (or you can also hard code them for testing purposes)
#Service(value = OauthAuthentication.class)
#Component(immediate = true, label = "My Oauth Authentication", description = "My Oauth Authentication", policy = ConfigurationPolicy.REQUIRE, metatype = true)
#Properties({
#Property(name = Constants.SERVICE_VENDOR, value = "ABC"),
#Property(name = "service.oauth.host", value = "", label = "Oauth Host", description = "Oauth Athentication Server"),
#Property(name = "service.oauth.url", value = "/service/oauth/token", label = "Oauth URL", description = "Oauth Authentication URL relative to the host"),
#Property(name = "service.oauth.clientid", value = "", label = "Oauth Client ID", description = "Oauth client ID to use in the authentication procedure"),
#Property(name = "service.oauth.clientsecret", value = "", label = "Oauth Client Secret", description = "Oauth client secret to use in the authentication procedure"),
#Property(name = "service.oauth.granttype", value = "", label = "Oauth Grant Type", description = "Oauth grant type") })
public class OauthAuthentication {
...
#Activate
private void activate(ComponentContext context) {
Dictionary<String, Object> properties = context.getProperties();
host = OsgiUtil.toString(properties, PROPERTY_SERVICE_OAUTH_HOST,new String());
// Similarly get all values
url =
clientID =
clientSecret =
grantType =
authType = "Basic" + " "+ Base64.encode(new String(clientID + ":" + clientSecret));
}
public static void getAuthorizationToken(
try {
UserManager userManager = resourceResolver.adaptTo(UserManager.class);
Session session = resourceResolver.adaptTo(Session.class);
// Getting the current user
Authorizable auth = userManager.getAuthorizable(session.getUserID());
user = auth.getID();
password = ...
...
...
String serviceURL = (host.startsWith("http") ? "": protocol + "://") + host + url;
httpclient = HttpClients.custom().build();
HttpPost httppost = new HttpPost(serviceURL);
// set params
ArrayList<BasicNameValuePair> formparams = new ArrayList<BasicNameValuePair>();
formparams.add(new BasicNameValuePair("username", user));
formparams.add(new BasicNameValuePair("password", password));
formparams.add(new BasicNameValuePair("client_id", clientID));
formparams.add(new BasicNameValuePair("client_secret",clientSecret));
formparams.add(new BasicNameValuePair("grant_type",grantType));
UrlEncodedFormEntity postEntity = new UrlEncodedFormEntity(formparams, "UTF-8");
httppost.setEntity(postEntity);
// set header
httppost.addHeader("Authorization", authType);
response = httpclient.execute(httppost);
HttpEntity entity = response.getEntity();
if (response.getStatusLine().getStatusCode() == 200) {
if (entity != null) {
object = new JSONObject(EntityUtils.toString(entity));
}
if (object != null) {
accessToken = object.getString("access_token");
////
}
}
}
I found the answer myself and thought I'd share the process I went through as well as the answer because it might help other people new to AEM.
How to find the cause of the error:
Go to CRXDE Lite.
Select console.
Then deselect the stop button to allow new console logs to appear (this is very counter-intuitive to me).
From here I was able to see the cause of the issue:
org.apache.sling.security.impl.ReferrerFilter Rejected empty referrer header for POST request to /oauth/token
Because postman does not place a referrer in the request header I had to tell Apache Sling to allow empty request headers.
To do this:
Go to /system/console/configMgr
Open the Apache Sling Referrer Filter Config
Select the Allow Empty check box
Good way to allow this to list the allowed hosts, otherwise this is against best practices for AEM security checklist.
Its fine for development environment not for production.