I'm trying to set up Thinktecture's Identity Server 3, but I can't seem to get it to return a refresh token when exchanging an authorization code (or when using the ResourceOwner flow, but I'm going to focus on the authorization code as it's more important to me right now). I get back access tokens and can use them to authenticate just fine, but it doesn't seem to even be generating the refresh tokens that I'm expecting to get back. Is there anything special that I need to do to get Identity Server to return refresh tokens?
I've looked through the documentation, but haven't seen anything that I've set up wrong, and the only thing on their page on refresh tokens that I'm not doing is explicitly requesting the "offline_access" scope when sending the user there for authentication, because whenever I try I get an "invalid scope" error. Therefore, I'm taking Thinktecture's phrasing of "Request the offline_access scope (via code or resource owner flow)" to mean that the offline_access scope is something automatically requested based on the flow you're using.
I've been trying to follow their sample applications (And the source code for the existing Owin Middleware from the Katana Project) as best I can, and my setup is as follows:
I've created a client using their client class, manually specifying the following:
var client = new Client()
{
ClientId = "SomeId",
ClientName = "Client with Authentication Code Flow",
RequireConsent = false, //Setting this to true didn't help
Flow = Flows.AuthorizationCode,
ClientSecrets = new List() {
new ClientSecret("secret")
},
RedirectUris = new List()
{
"localhost:/specific-redirect-path"
}
};
I'm making a call to the Authorization endpoint as follows:
var authorizationEndpoint =
AuthorizationEndpointBase +
"?client_id=" + Uri.EscapeDataString(Options.ClientId) +
"&scope=Default" +
"&response_type=code" +
"&redirect_uri=" + Uri.EscapeDataString(redirectUri) +
"&state=" + Uri.EscapeDataString(state);
Response.Redirect(authorizationEndpoint);
where "Default" is a scope I created.
In my callback, I call the token endpoint as follows:
IReadableStringCollection query = Request.Query;
string code = getValueFromQueryString("code", query);
var tokenRequestParameters = new List>()
{
new KeyValuePair("client_id", Options.ClientId),
new KeyValuePair("redirect_uri", GenerateRedirectUri()),
new KeyValuePair("client_secret", Options.ClientSecret),
new KeyValuePair("code", code),
new KeyValuePair("grant_type", "authorization_code"),
};
var requestContent = new FormUrlEncodedContent(tokenRequestParameters);
HttpResponseMessage response = await _httpClient.PostAsync(TokenEndpoint, requestContent, Request.CallCancelled);
response.EnsureSuccessStatusCode();
string oauthTokenResponse = await response.Content.ReadAsStringAsync();
When I make the call to the token endpoint, my logging on Identity Server displays the following (after the validation of the authorization code):
iisexpress.exe Information: 0 : [Thinktecture.IdentityServer.Core.Validation.TokenRequestValidator]: 7/13/2015 1:44:07 PM +00:00 -- Token request validation success
{
"ClientId": "SomeId",
"ClientName": "Client with Authentication Code Flow",
"GrantType": "authorization_code",
"AuthorizationCode": "f8f795e649044067ebd96a341c5af8c3"
}
iisexpress.exe Information: 0 : [Thinktecture.IdentityServer.Core.ResponseHandling.TokenResponseGenerator]: 7/13/2015 1:44:07 PM +00:00 -- Creating token response
iisexpress.exe Information: 0 : [Thinktecture.IdentityServer.Core.ResponseHandling.TokenResponseGenerator]: 7/13/2015 1:44:07 PM +00:00 -- Processing authorization code request
Debug: [Thinktecture.IdentityServer.Core.Services.Default.DefaultTokenService]: 7/13/2015 1:44:07 PM +00:00 -- Creating access token
Debug: [Thinktecture.IdentityServer.Core.Services.Default.DefaultTokenService]: 7/13/2015 1:44:07 PM +00:00 -- Creating reference access token
iisexpress.exe Information: 0 : [Thinktecture.IdentityServer.Core.Endpoints.TokenEndpointController]: 7/13/2015 1:44:07 PM +00:00 -- End token request
iisexpress.exe Information: 0 : [Thinktecture.IdentityServer.Core.Results.TokenResult]: 7/13/2015 1:44:07 PM +00:00 -- Returning token response.
I'm not sure what else would be pertinent, so I'll provide more information as needed.
You do have to explicitly ask for 'offline_access' in your request. Separate the other scopes you are requesting with a space. (In my examples below I am replacing 'Default' with 'MyApi' to be clear that we are talking about a scope defined by your app.)
&scope=MyApi offline_access
However, you must also grant that client the right to get refresh tokens, it doesn't just happen based on the flow you pick:
var client = new Client()
{
... //All the stuff you were doing before
ScopeRestrictions = new List<string>
{
"MyApi",
StandardScopes.OfflineAccess.Name, //"offline_access" -for refresh tokens
//Other commonly requested scopes:
//StandardScopes.OpenId.Name, //"openid"
//StandardScopes.Email.Name, //"email"
},
}
You may need to add 'offline_access' to your scope store as well. The scope store is the list of scopes that Identity Server knows about. Your question doesn't mention how your scope store is set up in your project, so you may already have it. But if the above doesn't immediately work for you, you may want to look around for code like this in the example you're working from and add OfflineAccess.
var scopeStore = new InMemoryScopeStore(new Scope[]{
StandardScopes.OpenId,
StandardScopes.Profile,
StandardScopes.Email,
StandardScopes.OfflineAccess, //<--- ensure this is here to allow refresh tokens
new Scope{
Enabled = true,
Name = "MyApi"
},
}
Add offline_access value in scope while sending token request
new Client
{
ClientId = "ro.angular",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.Address,
"api1",
IdentityServerConstants.StandardScopes.OfflineAccess
},
AllowOfflineAccess = true,
RefreshTokenUsage = TokenUsage.ReUse,
RefreshTokenExpiration = TokenExpiration.Sliding
}
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
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 new to Identity server and a key concept is missing from my understanding.
I am using the code from the MVC tutorial.
If I decorate my Home controller with the attribute [Authorize] and visit my website I get redirect to the IdentityServer. I then log in using my username and password. I then use some custom code and authenticate. I get back an AccessToken and I can then access the Home controller.
My client settings is as follows:
new Client {
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets = new List<Secret>{new Secret("secret".Sha256())},
RequireConsent = false,
AccessTokenLifetime = 1,
// where to redirect to after login
RedirectUris = new List<string>{"http://localhost:5002/signin-oidc"},
// where to redirect to after logout
PostLogoutRedirectUris = new List<string>{"http://localhost:5002"},
AllowedScopes = new List<string>
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
StandardScopes.OfflineAccess.Name,
}
}
My access token is
{
"nbf": 1474697839,
"exp": 1474697840,
"iss": "http://localhost:5000",
"aud": "http://localhost:5000/resources",
"client_id": "mvc",
"scope": [
"openid",
"profile"
],
"sub": "26296",
"auth_time": 1474697838,
"idp": "local",
"amr": [
"pwd"
]
}
As I set my AccessTokenLifetime to 1 my token when sent to call an API etc will be invalided. I still however will be able to access the website.
What is the best way to get the MVC website to confirm that my token has not expired? This may be where the refresh tokens come in to play.
Note
The AccessTokenLifetime set to 1 is for testing only so I can test things quickly.
You will need to set the user authentication lifetime to match that of your access token. If using OpenIdConnect you can do that with the following code:
.AddOpenIdConnect(option =>
{
...
option.Events.OnTicketReceived = async context =>
{
// Set the expiry time to match the token
if (context?.Properties?.Items != null && context.Properties.Items.TryGetValue(".Token.expires_at", out var expiryDateTimeString))
{
if(DateTime.TryParse(expiryDateTimeString, out var expiryDateTime))
{
context.Properties.ExpiresUtc = expiryDateTime.ToUniversalTime();
}
}
};
});
I assume you are using cookie authentication? If so you may have to switch off Sliding Expiration. Sliding Expiration will automatically refresh the cookie any time it processes a request which was more than halfway through the expiration window. However the access token won't be refreshed as part of this. Therefore you should let the user authentication lifetime run until the end, at which point it will expire and a new access token will be automatically retrieved using the refresh token.
.AddCookie(options =>
{
// Do not re-issue a new cookie with a new expiration time.
// We need to let it expire to ensure we get a fresh JWT within the token lifetime.
options.SlidingExpiration = false;
})
Maybe like this?
var user = HttpContext.Current.User.Identity;
if (!user.IsAuthenticated)
In below code I can retrieve refresh token successfully from email#company.com email addresses. However, when I try to login with email#outlook.com it doesn't give the refresh token instead it returns this response.
Response:
{
"error": "invalid_grant",
"error_description": "AADSTS70000: The provided value for the 'code' parameter is not valid. The code has expired.\r\nTrace ID: ...\r\nCorrelation ID: ...\r\nTimestamp: 2016-05-19 10:13:05Z",
"error_codes": [
70000
],
"timestamp": "2016-05-19 10:13:05Z",
"trace_id": "8cceb393-....",
"correlation_id": "5227de8...."
}
Code:
private async Task<string> GetRefreshRoken(string authCode, string onSuccessRedirectUri) {
var client = new HttpClient();
var parameters = new Dictionary<string, string>
{
{"client_id", _clientId},
{"client_secret", _clientSecret},
{"code",authCode }, // what retreived from //https://login.microsoftonline.com/common with authroization.
{"redirect_uri", onSuccessRedirectUri}, //http://localhost:27592/Home/Authorize
{"grant_type","authorization_code" }
};
var content = new FormUrlEncodedContent(parameters);
var response = await client.PostAsync("https://login.microsoftonline.com/common/oauth2/v2.0/token", content);
var tokensJsonString = await response.Content.ReadAsStringAsync();
dynamic token = Newtonsoft.Json.JsonConvert.DeserializeObject(tokensJsonString);
return token.refresh_token;
}
So I had googled with the error number and found http://www.matvelloso.com/2015/01/30/troubleshooting-common-azure-active-directory-errors/ page where the error describes:
Then I had changed my redirecting url to "http://localhost:27592/Home/Authorize/". Since I am using this https://dev.outlook.com/restapi/tutorial/dotnet tutorial as a reference , now I cannot login with any other account.
Is there any good approach to retrieve refresh tokens for outlook account?
For windows live id account, you will get error "The provided value for the 'code' parameter is not valid. The code has expired." when using the authorization code twice.
The correct way to refresh the token is using refresh token (v2.0 token reference > Refresh Token).
First, ensure you have declare the scope "offline_access".
Then, you will get the access_token when acquire the token using grant_type=code (the first time you acquire the token).
Next, you need to use grant_type=refresh_token to refresh your access token.