AuthenticationTokenProvider/Why context.SerializeTicket() and access_token not the same? - oauth-2.0

Why context.SerializeTicket() and access_token not the same?
public override async Task CreateAsync(AuthenticationTokenCreateContext context)
{
//if (string.IsNullOrEmpty(context.Ticket.Identity.Name)) return;
var clientId = context.OwinContext.Get<string>("as:client_id");
var refreshTokenId = Guid.NewGuid().ToString("n");
var refreshToken = new RefreshToken
{
Id = refreshTokenId,
ClientId = clientId,
UserName = context.Ticket.Identity.Name,
IssuedUtc = DateTime.Now,
ExpiresUtc = DateTime.Now.AddDays(30)
};
context.Ticket.Properties.IssuedUtc = refreshToken.IssuedUtc;
context.Ticket.Properties.ExpiresUtc = refreshToken.ExpiresUtc;
refreshToken.ProtectedTicket = context.SerializeTicket();
await _cacheManager.SetAsync(refreshTokenId, refreshToken, TimeSpan.FromDays(10));
context.SetToken(refreshTokenId);
}
Result:
refreshToken.ProtectedTicket = "M2NQCH_kdzOJyHj9Sc-i_WjZtwS1Tqe3WjFXf-Laja80RUw_cOIZ9IH-Bhh3rlZx64lYxj6IUILJsBNG5FPMm7edaJqYA-qEsqYJjLTSUgSEIikeiomxcQA4qK2397HwhhzwA10QNMLFAUVscEE8MzJ4kGn_qanMP-xW3YVMycQLgGBjC5pDqBd8Q1U0nHQ5Cwe1OvWrngnq06Qe5QvZfRrMppw8MyyoliqRtPyq0UU";
Postman:
{"access_token":"jL1f5jI16pZpAmeNIvOCXe7zMEi1WT3ElOt1Tau-vfDV3iBNm6cH6oXz4XzLW2Y-2Me2Hyvl09R3vHWedlYUTiGAmFK4m75jomeqNbK2L9nnOv8-1N6iaEpB2ppT9bYLF9c77SMtmcQoLG0iBpdNu9fVObl5u5W7M5yxi6Kjq5MYdYLdzczfkaP3QA1csHdWeiuCEHf9Dw6F8XKYRpqFqUiSzFcqUW3qXcWrTF1Hfsk","token_type":"bearer","expires_in":2591999,"refresh_token":"05d7b5373b8c490fb136afe985d756ab"}

access_token is the token issued when the user is authenticated.
context.SerializeTicket() serializes the refresh token.
Why context. SerializeTicket ( ) and access_token not the same?
They are different because they represent different things.
[bonus]
refresh_token contains the key used to retrieve and deserialize the refresh token. This is done inside the Receive or ReceiveAsync method.

Related

HTTP GET request to API behind AzureAD authentication with ASP.NET Core MVC

The code below gets a token which I then use to try and fetch some data from an API which is behind AzureAD authentication.
I get a token back, but when I use it to try and reach the API, I get "login to your account" in apiResponse.
What is wrong with my authorization?
var recoAadAppId = "xxxxxxxxxxxxxx";
var callerAadAppId = "xxxxxxxxxxxxxx";
var callerAadTenantId = "xxxxxxxxxxxxxx";
var token = await AcquireTokenWithSecret(callerAadAppId, callerAadTenantId, recoAadAppId);
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse(token.CreateAuthorizationHeader());
using (var response = await httpClient.GetAsync("https://redacted/app/rest/buildQueue"))
{
string apiResponse = await response.Content.ReadAsStringAsync();
}
public static Task<AuthenticationResult> AcquireTokenWithSecret(
string callerAadAppId, string callerTenantId, string recoAadAppId)
{
var secret = "mysecret";
var app = ConfidentialClientApplicationBuilder.Create(callerAadAppId).WithAuthority($"https://login.microsoftonline.com/{callerTenantId}").WithClientSecret(secret).Build();
var scopes = new[] { $"{recoAadAppId}/.default" };
return app.AcquireTokenForClient(scopes).ExecuteAsync(CancellationToken.None);
}

OpenIddict, after restarting auth server the tokens are invalidated

I have the following set up: Authorization server (.NET 6 with MVC, port 7000), Client (.NET 6 with MVC, port 7001), Resource Server (.NET 6 API, port 7002).
Authorization server set up:
builder.Services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = builder.Configuration["ClientId"];
options.ClientSecret = builder.Configuration["ClientSecret"];
});
builder.Services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
options.ClaimsIdentity.EmailClaimType = Claims.Email;
options.SignIn.RequireConfirmedAccount = false;
});
builder.Services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<AuthorizationContext>();
})
.AddServer(options =>
{
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo")
.SetIntrospectionEndpointUris("/connect/introspect");
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles);
options.AllowAuthorizationCodeFlow();
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableStatusCodePagesIntegration();
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
builder.Services.AddHostedService<Worker>();
The seeded clients:
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
ConsentType = ConsentTypes.Explicit,
DisplayName = "MVC client application",
PostLogoutRedirectUris =
{
new Uri("https://localhost:7001/signout-callback-oidc")
},
RedirectUris =
{
new Uri("https://localhost:7001/signin-oidc")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "api1"
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});
// resource server
if (await manager.FindByClientIdAsync("resource_server_1") == null)
{
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = "resource_server_1",
ClientSecret = "846B62D0-DEF9-4215-A99D-86E6B8DAB342",
Permissions =
{
Permissions.Endpoints.Introspection
}
};
await manager.CreateAsync(descriptor);
}
Client config:
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/login";
options.ExpireTimeSpan = TimeSpan.FromMinutes(50);
options.SlidingExpiration = false;
})
.AddOpenIdConnect(options =>
{
options.ClientId = "mvc";
options.ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654";
options.RequireHttpsMetadata = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.Code;
options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
options.Authority = "https://localhost:7000/";
options.Scope.Add("email");
options.Scope.Add("roles");
options.Scope.Add("api1");
options.MapInboundClaims = false;
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
});
Resource Server config:
builder.Services.AddOpenIddict()
.AddValidation(options =>
{
options.SetIssuer("https://localhost:7000/");
options.AddAudiences("resource_server_1");
options.UseIntrospection()
.SetClientId("resource_server_1")
.SetClientSecret("846B62D0-DEF9-4215-A99D-86E6B8DAB342");
options.UseSystemNetHttp();
options.UseAspNetCore();
});
builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
This is how the client makes request to resource server:
[Authorize, HttpPost("~/")]
public async Task<ActionResult> Index(CancellationToken cancellationToken)
{
var token = await HttpContext.GetTokenAsync(CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectParameterNames.AccessToken);
if (string.IsNullOrEmpty(token))
{
throw new InvalidOperationException("The access token cannot be found in the authentication ticket. " +
"Make sure that SaveTokens is set to true in the OIDC options.");
}
using var client = _httpClientFactory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:7002/api/message");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
using var response = await client.SendAsync(request, cancellationToken);
var content = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();
return View("Home", model: await response.Content.ReadAsStringAsync());
}
The problem is following when I set up those 3 instances (auth server, client, resource server) and I am NOT authenticated in the client (no cookies). I can authenticate on the client (and therefore on auth server). Then I make the request from the client to the resource server and it returns 200.
But then I stop all 3 instances and try to do it again.
At that time I'm already authenticated in the client (cookies) and can extract token (FYI the tokens are the same between requests before stopping instances and after). But this token is invalid and the response code from the resource server is 401.
On the resource server logs I can see the following logs: "OpenIddict.Validation.AspNetCore was not authenticated. Failure message: An error occurred while authenticating the current request", and "invalid_token, the specified token is invalid"
The question: is it expected behavior? I assume the reason is that data protection changed key ring or something like that. If it is expected - then how to do redeploys without reauthenticating all the users?
I'm pretty sure the problem is with this line
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
Those certificates (keys) are changed when you restart the application. You need to have production encryption/signing keys in place.
See options.AddEncryptionKey and options.AddSigningKey
Key can be created like this
var rsa = RSA.Create(2048);
var key = new RsaSecurityKey(rsa);
You can get the XML for the key and save it somewhere PRIVATE
var xml = key.Rsa.ToXmlString(true);
When you start the application, you load the key using the XML
var rsa = RSA.Create();
rsa.FromXmlString(xml);
Then you add the key to openiddict
options.AddEncryptionKey(rsa);
options.AddSigningKey(rsa);
You may also want to use the following methods instead
options.AddEncryptionCredentials
options.AddSigningCredentials
options.AddEncryptionCertificate
options.AddSigningCertificate
It depends of what you have available.

VerifyUserTokenAsync returns false when the user is not persisted

Considering PhoneNumber as my customized user's Username, I need only users whose phone number is verified to be able to Sign-Up. So I created a temp user and generated verification token for him/her and sent back the token to provided phone number as follows:
public Task Handle(SendSignupSmsRequest request)
{
var user = new CustomUser { UserName = request.PhoneNumber, PhoneNumber = request.PhoneNumber };
var token = await _userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, "sign-up");
// send token to provided phone number
}
In sign-up request handler, I tried to re-create the same temp user and verify the token as follows:
public Task Handle(SignupRequest request)
{
var user = new CustomUser { UserName = request.PhoneNumber, PhoneNumber = request.PhoneNumber };
var tokenVerified = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, "sign-up", request.Token);
if (!tokenVerified)
// do something;
else
// do something else
}
I see that tokenVerified is always False! I tried the following to find what is wrong with my code:
Verify token with the same temp user ====> successful verification
var user = new CustomUser { UserName = request.PhoneNumber, PhoneNumber = request.PhoneNumber };
var token = await _userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, "sign-up");
var tokenVerified = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, "sign-up", request.Token);
Verify token with the new temp user created just like temp user ====> unsuccessful verification
var user = new CustomUser { UserName = request.PhoneNumber, PhoneNumber = request.PhoneNumber };
var token = await _userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, "sign-up");
user = new CustomUser { UserName = request.PhoneNumber, PhoneNumber = request.PhoneNumber };
var tokenVerified = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, "sign-up", request.Token);
It all comes down to SecurityStamp!
First, assign a temporal security stamp to user created in send sign-up sms request handler. Then in sign-up request handler set the same security stamp for the re-created user. Doing so, the token will be verified successfully.

Oauth refresh token

Sorry for the newbie question.
What the point of refresh token if i have to send my credentials anyway?
Or in another words, what the practical difference between requesting refresh token or just a new access token?
here is my code:
TokenResponse tokenResponse = null;
var tokenRequest = new TokenRequest
{
GrantType = "customgrant",
ClientId = "myUserName",
ClientSecret = "secret",
Address = disco.TokenEndpoint,
};
tokenResponse = await client.RequestTokenAsync(tokenRequest);
var testClient = new HttpClient();
testClient.SetBearerToken(tokenResponse.AccessToken);
var apiRes = await testClient.GetAsync("http://localhost:11000/api/GetData/104246");
if (!apiRes.IsSuccessStatusCode)
{
if (apiRes.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
RefreshTokenRequest refreshTokenRequest = new RefreshTokenRequest();
refreshTokenRequest.RefreshToken = tokenResponse.RefreshToken;
refreshTokenRequest.ClientId = "myUserName";
refreshTokenRequest.ClientSecret = "secret";
refreshTokenRequest.Address = disco.TokenEndpoint;
tokenResponse = await testClient.RequestRefreshTokenAsync(refreshTokenRequest);
}
}
Your question is not clear. refresh token is used to get new valid access token and you don't need user credentials, when you already have valid refresh token.

Getting OAuth verifier value as null

I am trying to get access token for which I need OAuth verifier. I am using TokenSecretVerifierHolder class to get token, token secret and verifier but I am only getting the value of token, token secret, however the verifier value is coming aa null. Here is my code.
public TokenSecretVerifierHolder getRequestToken() {
try {
OAuthAccessor accessor = getAccessor();
OAuthClient oAuthClient = new OAuthClient(new HttpClient4());
List<OAuth.Parameter> callBack;
if ((this.callback == null) || ("".equals(this.callback))) {
callBack = Collections.emptyList();
} else {
callBack = ImmutableList.of(new OAuth.Parameter("oauth_callback", this.callback));
}
OAuthMessage message = oAuthClient.getRequestTokenResponse(accessor, "POST", callBack);
TokenSecretVerifierHolder tokenSecretVerifier = new TokenSecretVerifierHolder();
tokenSecretVerifier.token = accessor.requestToken;
tokenSecretVerifier.secret = accessor.tokenSecret;
tokenSecretVerifier.verifier = message.getParameter("oauth_verifier");
return tokenSecretVerifier;
} catch (Exception e) {
throw new RuntimeException("Failed to obtain request token", e);
}
}
private String getAccessToken() {
AtlassianOAuthClientRequest jiraoAuthClient = getJiraOAuthClient();
TokenSecretVerifierHolder requestToken = jiraoAuthClient.getRequestToken();
String authorizeUrl = jiraoAuthClient.getAuthorizeUrlForToken(requestToken.token);
String token = requestToken.token;
String tokenSecret = requestToken.secret;
String verifier = requestToken.verifier;
String accessToken = jiraoAuthClient.swapRequestTokenForAccessToken(token, tokenSecret, verifier);
String verifier = requestToken.verifier;
System.out.println("Access token is : " + accessToken);
return accessToken;
}
After retreiving the value of token, token secret and verifier I am passing it to get the access token value. What I want is that after I acknowledge the authorizeUrl it should return me the value of Oauth verifier. In my case its returning as null.
The required value of verifier is returned to you during HTTP redirect to your callback URL - the redirect happens with URL like https://consumer.url/?oauth_token=TOKEN&oauth_verifier=VERIFIER, so value of oauth_verifier parameter is what you need.

Resources