OpenIddict, after restarting auth server the tokens are invalidated - oauth

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.

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);
}

Sending mail using GraphServiceClient

I wrote a dll using .NET C# that was supposed to send emails using graph API.
When I'm using the dll from a console application - everything works as expected: if the user is logged in the mail is sent, and if not - a screen pops up to connect.
But, when I try to use the same dll in WinForms, the program stuck.
Any idea why?
This is my code:
var options = new PublicClientApplicationOptions {
ClientId = clientId,
TenantId = tenantId,
RedirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient",
};
if (application == null) {
application = PublicClientApplicationBuilder.CreateWithApplicationOptions(options).WithAuthority(AzureCloudInstance.AzurePublic, ClientSecretOrTenantId).Build();
}
string token = "";
GraphServiceClient graphServiceClient = new GraphServiceClient(new DelegateAuthenticationProvider(async(requestMessage) =>{
token = await GetToken();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}));
Recipient recipient = new Recipient();
recipient.EmailAddress = new EmailAddress();
recipient.EmailAddress.Address = toAddress;
List < Recipient > recipients = new List < Recipient > ();
recipients.Add(recipient);
var message = new Message {
Subject = subject,
Body = new ItemBody {
ContentType = isHtml ? BodyType.Html: BodyType.Text,
Content = bodyText,
},
ToRecipients = recipients,
};
try {
await graphServiceClient.Me.SendMail(message, false).Request().PostAsync(); // get stuck here
} catch(ServiceException) {
graphServiceClient = new GraphServiceClient(new DelegateAuthenticationProvider(async(requestMessage) =>{
token = await GetToken();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}));
await graphServiceClient.Me.SendMail(message, false).Request().PostAsync();
}
I'd hazard a guess that you're trying to make the asynchronous method synchronous by calling SendEmailAsync(email).Wait() in your (button click?) event handler, which is causing a WinForms UI thread lock.
The solution is to mark your event handler as async void and await your method in the event handler code.

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.

JwtFormat: why does it add the token's issuer to the ValidIssuers property?

I'm taking a look at the source code of the JwtFormat class and I'm wondering why does it add the Issuer it recovers from the token to the list of ValidIssuers. Does that mean that it will accept all issuers as valid if I don't specify a key or provide a IssueValidator handler to the TokenValidationParameters that are being used?
Btw, I'm lookit at this class because I'm investigating an issue regarding the use of JWT tokens (azure ad v2.0) in a web api app that seems to be ignoring the ValidIssuer property:
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions {
AccessTokenFormat = new JwtFormat(
GetTokenValidationParameters(),
new OpenIdConnectCachingSecurityTokenProvider(authority)),
Provider = new OAuthBearerAuthenticationProvider {
OnValidateIdentity = ValidateIdentity
}
});
private TokenValidationParameters GetTokenValidationParameters() {
return new TokenValidationParameters {
ValidAudience = ConfigData.ClientId,
ValidIssuer = "nobody",
ValidIssuers = null,
IssuerValidator = ValidateIssuer
};
}
I'm editing this to give more information about what's going on.
According to the source code, ValidateIssuer is true by default, so there's no need to set it again. Just to be sure, here's the source code:
public TokenValidationParameters()
{
this.RequireExpirationTime = true;
this.RequireSignedTokens = true;
this.SaveSigninToken = false;
this.ValidateActor = false;
this.ValidateAudience = true;
this.ValidateIssuer = true;
this.ValidateIssuerSigningKey = false;
this.ValidateLifetime = true;
}
I'm setting up the IssuerValidator because I want to make sure that if the ValidIssuer is set, then I want to compare the token's issuer with that value (and don't want to check against the ValidIssuers collection when the ValidIssuer's validation fails).
In case you're wondering where the ValidIssuers is being filled (and yes, even in my example, it's being automatically populated, even though I've set it explicityl to null), it's happening in JwtFormat's Unprotect method:
public AuthenticationTicket Unprotect(string protectedText)
{
if (string.IsNullOrWhiteSpace(protectedText))
throw new ArgumentNullException(nameof (protectedText));
if (!(this.TokenHandler.ReadToken(protectedText) is JwtSecurityToken))
throw new ArgumentOutOfRangeException(nameof (protectedText), Microsoft.Owin.Security.Jwt.Properties.Resources.Exception_InvalidJwt);
TokenValidationParameters validationParameters = this._validationParameters;
if (this._issuerCredentialProviders != null)
{
validationParameters = validationParameters.Clone();
IEnumerable<string> second1 = this._issuerCredentialProviders.Select<IIssuerSecurityTokenProvider, string>((Func<IIssuerSecurityTokenProvider, string>) (provider => provider.Issuer));
validationParameters.ValidIssuers = validationParameters.ValidIssuers != null ? validationParameters.ValidIssuers.Concat<string>(second1) : second1;
IEnumerable<SecurityToken> second2 = this._issuerCredentialProviders.Select<IIssuerSecurityTokenProvider, IEnumerable<SecurityToken>>((Func<IIssuerSecurityTokenProvider, IEnumerable<SecurityToken>>) (provider => provider.SecurityTokens)).Aggregate<IEnumerable<SecurityToken>>((Func<IEnumerable<SecurityToken>, IEnumerable<SecurityToken>, IEnumerable<SecurityToken>>) ((left, right) => left.Concat<SecurityToken>(right)));
validationParameters.IssuerSigningTokens = validationParameters.IssuerSigningTokens != null ? validationParameters.IssuerSigningTokens.Concat<SecurityToken>(second2) : second2;
}
SecurityToken validatedToken;
ClaimsIdentity identity = (ClaimsIdentity) this.TokenHandler.ValidateToken(protectedText, validationParameters, out validatedToken).Identity;
AuthenticationProperties properties = new AuthenticationProperties();
if (this.UseTokenLifetime)
{
DateTime validFrom = validatedToken.ValidFrom;
if (validFrom != DateTime.MinValue)
properties.IssuedUtc = new DateTimeOffset?((DateTimeOffset) validFrom.ToUniversalTime());
DateTime validTo = validatedToken.ValidTo;
if (validTo != DateTime.MinValue)
properties.ExpiresUtc = new DateTimeOffset?((DateTimeOffset) validTo.ToUniversalTime());
properties.AllowRefresh = new bool?(false);
}
return new AuthenticationTicket(identity, properties);
}
Btw, this method ends up being called (indirectly) by the AuthenticateCoreAsync method when it needs to deserialize the token:
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
try
{
string requestToken = (string) null;
string authorization = this.Request.Headers.Get("Authorization");
if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
requestToken = authorization.Substring("Bearer ".Length).Trim();
OAuthRequestTokenContext requestTokenContext = new OAuthRequestTokenContext(this.Context, requestToken);
await this.Options.Provider.RequestToken(requestTokenContext);
if (string.IsNullOrEmpty(requestTokenContext.Token))
return (AuthenticationTicket) null;
AuthenticationTokenReceiveContext tokenReceiveContext = new AuthenticationTokenReceiveContext(this.Context, this.Options.AccessTokenFormat, requestTokenContext.Token);
await this.Options.AccessTokenProvider.ReceiveAsync(tokenReceiveContext);
if (tokenReceiveContext.Ticket == null)
tokenReceiveContext.DeserializeTicket(tokenReceiveContext.Token);
//remaining code removed
}
Since I really haven't read the specs, I was wondering if anyone could explain me this behavior (of always adding the token's issuer to the ValidIssuers collection and checking if the token's issuer is in the ValidIssuers - which will always be true!)
Final edit
Ok, my bad...Not enough coffee, I think...In fact, the issuer is being added not from the token itself, but from the IIssuerSecurityTokenProvider that is passed to the JwtFormat ctor (gets it from the metadata endpoint)...
Sorry guys...
Thanks.
Luis
I'm currently without Vs, because I'm writing on my mobile phone, there should be a ValidateIssuer Property in the TokenValidationParameters, but it looks like you set the IssuerValidator to ValidateIssuer, which should be true, so try it that way:
private TokenValidationParameters GetTokenValidationParameters() {
return new TokenValidationParameters {
ValidAudience = ConfigData.ClientId,
ValidIssuer = "nobody",
ValidateIssuer = true
};
}

SurveyMonkey returning unexpected EOF error exchanging authorization code for access token

Code that had been working in a "localhost" environment is failing after deploying it to production under (obviously) a new callback URL. Nothing else was changed. The request to SurveyMonkey's API to exchange the Authorization Code for a long-lived Access Token is returning this error:
System.Net.WebException: The underlying connection was closed: An unexpected error occurred on a send. ---> System.IO.IOException: Received an unexpected EOF or 0 bytes from the transport stream.
Is there something else that needs to be changed, maybe to a production client_id ? Please advise, we are stuck.
Here is the code:
var clientId = _sds.ByName("SurveyMonkeyClientID").Value;
var apiSecret = _sds.ByName("SurveyMonkeyAPISecret").Value;
var callbackURL = _sds.ByName("SurveyMonkeyCallbackURL").Value;
var accessTokenUri = "https://api.surveymonkey.net/oauth/token?api_key=" + _sds.ByName("SurveyMonkeyAPIKey").Value;
// Create postback data
var postData = new NameValueCollection()
{
{ "client_secret", apiSecret },
{ "code", authorizationCode },
{ "redirect_uri", callbackURL },
{ "client_id", clientId },
{ "grant_type", "authorization_code" }
};
var queryString = string.Join("&", (
from key in postData.AllKeys
from value in postData.GetValues(key)
select string.Format("{0}={1}", HttpUtility.UrlEncode(key), HttpUtility.UrlEncode(value))).ToArray()
);
// Create and configure the web request object
var request = WebRequest.Create(accessTokenUri);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
var dataStream = request.GetRequestStream();
var postArray = Encoding.UTF8.GetBytes(queryString);
dataStream.Write(postArray, 0, postArray.Length);
dataStream.Close();
// Make the call to Survey Monkey's token exchange uri
try
{
var response = request.GetResponse();
dataStream = response.GetResponseStream();
var reader = new StreamReader(dataStream);
var responseFromServer = reader.ReadToEnd();
reader.Close();
dataStream.Close();
response.Close();
SaveSurveyMonkeyJsonWhole(responseFromServer);
dynamic jobject = _serializer.DeserializeObject(responseFromServer);
var access_token = jobject["access_token"];
return access_token;
}
catch (Exception ex)
{
ErrorSignal.FromCurrentContext().Raise(ex);
return string.Empty;
}

Resources