I have multi tenant application where each tenant can use different IdP to authenticate. Below code correctly redirects to IdP but problem is to get back the response to ACS endpoint.
Key is the Configuration method which configures the paths and their authentication:
[assembly: OwinStartup(typeof(SSOSamlDemoASPNET.App_Start.Startup))]
namespace SSOSamlDemoASPNET.App_Start
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/client/okta", (appx) =>
{
ConfigureAuthentication(appx, "/client/okta/Saml2", ...);
});
app.Map("/client/azuread", (appx) =>
{
ConfigureAuthentication(appx, "/client/azuread/Saml2", ...);
});
}
private static void ConfigureAuthentication(IAppBuilder app, string modulePath, string audience, string issuer, string metadataUrl)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
CookieName = "LoggedUser",
CookiePath = "/",
CookieManager = new SystemWebCookieManager(),
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
ConfigureSaml(app, modulePath, audience, issuer, metadataUrl);
}
private static void ConfigureSaml(IAppBuilder app, string modulePath, string audience, string issuer, string metadataUrl)
{
var saml2options = new Saml2AuthenticationOptions(false);
var spOptions = new SPOptions
{
EntityId = new EntityId(audience),
ModulePath = modulePath,
PublicOrigin = new Uri("https://localhost:44340/"),
};
spOptions.Logger = new ConsoleLoggerAdapter();
saml2options.SPOptions = spOptions;
saml2options.IdentityProviders.Add(new IdentityProvider(new EntityId(issuer), spOptions)
{
AllowUnsolicitedAuthnResponse = true,
MetadataLocation = metadataUrl,
LoadMetadata = true,
Binding = Saml2BindingType.HttpPost,
});
app.UseSaml2Authentication(saml2options);
}
}
}
Authenticating against individual IdP is done like this:
authProperties.Dictionary["idp"] = "https://sts.windows.net/xxx/";
authProperties.RedirectUri = "https://localhost:44340/client/azuread/ExternalLoginCallback";
HttpContext.Current.Request.GetOwinContext().Authentication.Challenge(authProperties, "Saml2");
When inspecting code of the Sustainsys.Saml2 library (especially Saml2AuthenticationHandler). I found the conditions do not take into account OwinRequest.PathBase and therefore the identity is not coming back to the application.
An example can be (Saml2AuthenticationHandler.Invoke method).
Options.SPOptions.ModulePath = /client/azuread/Saml2
Request.Path = /Saml2/Acs
==> therefore the code inside the condition is not executed.
public override async Task<bool> InvokeAsync()
{
var Saml2Path = new PathString(Options.SPOptions.ModulePath);
if (Request.Path.StartsWithSegments(Saml2Path, out PathString remainingPath))
{
if (remainingPath == new PathString("/" + CommandFactory.AcsCommandName))
{
var ticket = (MultipleIdentityAuthenticationTicket)await AuthenticateAsync();
if (ticket.Identities.Any())
{
Context.Authentication.SignIn(ticket.Properties, ticket.Identities.ToArray());
// No need to redirect here. Command result is applied in AuthenticateCoreAsync.
}
else
{
Response.Redirect(ticket.Properties.RedirectUri);
}
return true;
}
Is there any way to change this behavioral? e.g. saml2Options.Notifications to get this working?
That is obviously a bug/lack of feature, but nothing that will be fixed on the Owin module - it's on life support.
The solution for a multi tenancy owin app is to register one Saml2 middleware and add multiple IdentityProviders to that one. The middleware will handle all responses on the same endpoint and use the configuration from the right IdentityProvider based on where the response came from.
Related
I have an asp.net 4.6 web forms application (no MVC). I am updating the security in my application. I am using OpenIdConnectAuthentication to authenticate with our Azure AD. Then I pass the access token to Microsoft graph to send an email with Office 365. My token is set to expire in 60 minutes. I either need to expand the expiration to 8 hours or refresh the token. Without having MVC I am not sure how to handle this. I am looking for help with direction to take and possibly code samples.
(I original tried to utilize an MVC sample and put it into my project using a Session Token class. Once we tested with multiple users I believe I had a memory leak and it would crash in about 5 minutes.)
Startup code:
public class Startup
{
private readonly string _clientId = ConfigurationManager.AppSettings["ClientId"];
private readonly string _redirectUri = ConfigurationManager.AppSettings["RedirectUri"];
private readonly string _authority = ConfigurationManager.AppSettings["Authority"];
private readonly string _clientSecret = ConfigurationManager.AppSettings["ClientSecret"];
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieManager = new SystemWebCookieManager(),
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = _clientId,
ClientSecret = _clientSecret,
//Authority = _authority,
Authority = String.Format(_authority, domain, "/v2.0"),
RedirectUri = _redirectUri,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Scope = OpenIdConnectScope.OpenIdProfile,
UseTokenLifetime = false,
TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name", RequireExpirationTime = false},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// Exchange code for access and ID tokens
var auth = String.Format(_authority, "common/oauth2/v2.0", "/token");
var tokenClient = new TokenClient($"{auth}", _clientId, _clientSecret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, _redirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
var claims = new List<Claim>()
{
new Claim("id_token", tokenResponse.IdentityToken),
new Claim("access_token", tokenResponse.AccessToken)
};
n.AuthenticationTicket.Identity.AddClaims(claims);
},
},
});
}
}
SDK Helper:
public class SDKHelper
{
// Get an authenticated Microsoft Graph Service client.
public static GraphServiceClient GetAuthenticatedClient()
{
GraphServiceClient graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
string accessToken = System.Security.Claims.ClaimsPrincipal.Current.FindFirst("access_token").Value;
// Append the access token to the request.
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
// Get event times in the current time zone.
requestMessage.Headers.Add("Prefer", "outlook.timezone=\"" + TimeZoneInfo.Local.Id + "\"");
// This header has been added to identify our sample in the Microsoft Graph service. If extracting this code for your project please remove.
requestMessage.Headers.Add("SampleID", "aspnet-snippets-sample");
}));
return graphClient;
}
}
Sending Email:
GraphServiceClient graphClient = SDKHelper.GetAuthenticatedClient();
string address = emailaddress;
string guid = Guid.NewGuid().ToString();
List<Recipient> recipients = new List<Recipient>();
recipients.Add(new Recipient
{
EmailAddress = new Microsoft.Graph.EmailAddress
{
Address = address
}
});
// Create the message.
Message email = new Message
{
Body = new ItemBody
{
ContentType = Microsoft.Graph.BodyType.Text,
},
Subject = "TEST",
ToRecipients = recipients,
From = new Recipient
{
EmailAddress = new Microsoft.Graph.EmailAddress
{
Address = address
}
}
};
// Send the message.
try
{
graphClient.Me.SendMail(email, true).Request().PostAsync().Wait();
}
catch (ServiceException exMsg)
{
}
You need to request the scope offline_access. Once you've requested that, the /token endpoint will return both an access_token and a refresh_token. When your token expires, you can make another call to the /token endpoint to request a new set of access and refresh tokens.
You might find this article helpful: Microsoft v2 Endpoint Primer. In particular, the section on refresh tokens.
When starting up a fresh .Net Core 2.0 project with Azure AD Authentication you get a working sample that can sign in to your tenant, great!
Now I want to get an access token for the signed in user and use that to work with Microsoft Graph API.
I am not finding any documentation on how to achieve this. I just want a simple way to get an access token and access the graph API, using the template created when you start a new .NET Core 2.0 project. From there I should be able to figure out the rest.
Very important that it works with the project that gets created when following the process where you select Work and school accounts for authentication when creating a new 2.0 MVC Core app in Visual Studio.
I wrote a blog article which shows just how to do that: ASP.NET Core 2.0 Azure AD Authentication
The TL;DR is that you should add a handler like this for when you receive an authorization code from AAD:
.AddOpenIdConnect(opts =>
{
Configuration.GetSection("Authentication").Bind(opts);
opts.Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = async ctx =>
{
var request = ctx.HttpContext.Request;
var currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path);
var credential = new ClientCredential(ctx.Options.ClientId, ctx.Options.ClientSecret);
var distributedCache = ctx.HttpContext.RequestServices.GetRequiredService<IDistributedCache>();
string userId = ctx.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
var cache = new AdalDistributedTokenCache(distributedCache, userId);
var authContext = new AuthenticationContext(ctx.Options.Authority, cache);
var result = await authContext.AcquireTokenByAuthorizationCodeAsync(
ctx.ProtocolMessage.Code, new Uri(currentUri), credential, ctx.Options.Resource);
ctx.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
};
});
Here my context.Options.Resource is https://graph.microsoft.com (Microsoft Graph), which I'm binding from config along with other settings (client id etc.).
We redeem a token using ADAL, and store the resulting token in a token cache.
The token cache is something you will have to make, here is the example from the example app:
public class AdalDistributedTokenCache : TokenCache
{
private readonly IDistributedCache _cache;
private readonly string _userId;
public AdalDistributedTokenCache(IDistributedCache cache, string userId)
{
_cache = cache;
_userId = userId;
BeforeAccess = BeforeAccessNotification;
AfterAccess = AfterAccessNotification;
}
private string GetCacheKey()
{
return $"{_userId}_TokenCache";
}
private void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
Deserialize(_cache.Get(GetCacheKey()));
}
private void AfterAccessNotification(TokenCacheNotificationArgs args)
{
if (HasStateChanged)
{
_cache.Set(GetCacheKey(), Serialize(), new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1)
});
HasStateChanged = false;
}
}
}
The token cache here uses a distributed cache to store tokens, so that all instances serving your app have access to the tokens. They are cached per user, so you can retrieve a token for any user later.
Then when you want to get a token and use MS graph, you'd do something like (important stuff in GetAccessTokenAsync()):
[Authorize]
public class HomeController : Controller
{
private static readonly HttpClient Client = new HttpClient();
private readonly IDistributedCache _cache;
private readonly IConfiguration _config;
public HomeController(IDistributedCache cache, IConfiguration config)
{
_cache = cache;
_config = config;
}
[AllowAnonymous]
public IActionResult Index()
{
return View();
}
public async Task<IActionResult> MsGraph()
{
HttpResponseMessage res = await QueryGraphAsync("/me");
ViewBag.GraphResponse = await res.Content.ReadAsStringAsync();
return View();
}
private async Task<HttpResponseMessage> QueryGraphAsync(string relativeUrl)
{
var req = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0" + relativeUrl);
string accessToken = await GetAccessTokenAsync();
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
return await Client.SendAsync(req);
}
private async Task<string> GetAccessTokenAsync()
{
string authority = _config["Authentication:Authority"];
string userId = User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
var cache = new AdalDistributedTokenCache(_cache, userId);
var authContext = new AuthenticationContext(authority, cache);
string clientId = _config["Authentication:ClientId"];
string clientSecret = _config["Authentication:ClientSecret"];
var credential = new ClientCredential(clientId, clientSecret);
var result = await authContext.AcquireTokenSilentAsync("https://graph.microsoft.com", credential, new UserIdentifier(userId, UserIdentifierType.UniqueId));
return result.AccessToken;
}
}
There we acquire a token silently (using the token cache), and attach it to requests to the Graph.
I have two Web APIs with a shared machine.key. I would like to pass the bearer token generated by the first Web API to the second Web API as a parameter (i.e. token=xxxxxxxx) and extract the identity claims (i.e userId) from it.
Is this possible? I've looked all over but there doesn't seem to be much information on parsing a text bearer token to extract claims.
Thanks.
If you're using OWIN, you could implement your own OAuthBearerAuthenticationProvider, which takes the token from the query string and sets it to the context:
internal class MyAuthProvider : OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
if (context.Token == null)
{
var value = context.Request.Query.Get("token");
if (!string.IsNullOrEmpty(value))
{
context.Token = value;
}
}
return Task.FromResult<object>(null);
}
}
You could use it in your Startup.cs like this:
public void Configuration(IAppBuilder app)
{
// All the other stuff here
var audience = "";
var secret = "...";
app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions
{
Provider = new MyAuthProvider(),
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new [] { audience },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider("MyApp", TextEncodings.Base64Url.Decode(key))
}
});
// All the other stuff here
}
When you've implemented your auth like this, you can access the token information in your WebApi controller via the User.Identity property. To read custom claims, you can cast it to ClaimsIdentity.
var identity = User.Identity as ClaimsIdentity;
var myClaim = identity.Claims.FirstOrDefault(c => c.Type == "myClaimKey");
I have an asp.net core application where I am configuring cookie authentication and OpenID Connect authentication. I am also using Session and a distributed SQL server cache.
In the cookie authentication configuration, I am setting the SessionStore property to use my distributed cache ticket store. I have my session timeout set to 60 minutes. When I put items into session directly, it uses the sql cache and the entries show a 60 minute sliding expire time. That all works fine. Now, when the cookie auth uses the distributed cache ticket store, i can see the entries in the database as well (with a sliding 60 minute timeout). But if I let a web page sit for 20 minutes or more and then refresh the page, the ticket store removes the cache entry in the database for the cookie; even though the full 60 minutes have not passed. When debugging the ticket store, i see the call to Retrieve get called, then a call to Remove, and then another call to Retrieve; which at that point there is no more cache entry.
I'm sure I'm missing some setting somewhere, but the just the cookie cache entries are being cleaned up and removed prematurely. I cannot figure out why.
Here are the relevant parts of my startup:
public class Startup
{
// ...
public void ConfigureServices(IServiceCollection services)
{
// app insights
services.AddApplicationInsightsTelemetry(this.Configuration);
// authentication
services.AddAuthentication(options => options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);
// options
services.AddOptions()
.Configure<ConfigurationOptions>(this.Configuration)
.AddUtilitiesLayerConfigurationOptions(this.Configuration)
.AddDataLayerConfigurationOptions(this.Configuration)
.AddServicesLayerConfigurationOptions(this.Configuration)
.AddWebAppLayerConfigurationOptions(this.Configuration);
// caching/session
services.AddDistributedSqlServerCache(this.ConfigureSqlServerCacheOptions);
services.AddSession(this.ConfigureSessionOptions);
// mvc
services.AddMvc(ConfigureMvcOptions);
// custom services
services
.AddConfigurationLayerServices()
.AddCommonLayerServices()
.AddUtilitiesLayerServices()
.AddServicesLayerServices(this.Configuration);
}
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
IServiceProvider serviceProvider,
ILoggerFactory loggerFactory,
ITicketStore distributedCacheTicketStore)
{
// setup logging
loggerFactory.AddDebug();
// app insights request telemetry (this must be first)
app.UseApplicationInsightsRequestTelemetry();
// exceptions
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage(new DeveloperExceptionPageOptions { SourceCodeLineCount = 10 });
}
else
{
app.UseExceptionHandler("/error");
}
// app insights exception telemetry (right after exception config)
app.UseApplicationInsightsExceptionTelemetry();
// session
app.UseSession();
// status code pages (redirect to error controller)
app.UseStatusCodePagesWithRedirects("/error/{0}");
// static files
// this is before auth, so no static files require auth
// if we wanth auth for static files, move this after the auth middleware
app.UseStaticFiles();
// auth
app.UseCookieAuthentication(this.BuildCookieAuthenticationOptions(distributedCacheTicketStore));
app.UseOpenIdConnectAuthentication(this.BuildOpenIdConnectOptions());
// mvc
app.UseMvc();
}
private CookieAuthenticationOptions BuildCookieAuthenticationOptions(ITicketStore ticketStore)
{
var configuration = new ConfigurationOptions();
this.Configuration.Bind(configuration);
return new CookieAuthenticationOptions
{
CookieSecure = CookieSecurePolicy.SameAsRequest,
CookieName = configuration.Session.AuthenticationCookieName,
AccessDeniedPath = "/access-denied",
SessionStore = ticketStore
};
}
private OpenIdConnectOptions BuildOpenIdConnectOptions()
{
var configuration = new ConfigurationOptions();
this.Configuration.Bind(configuration);
return new OpenIdConnectOptions
{
ClientId = configuration.AzureActiveDirectory.ClientID,
Authority = configuration.AzureActiveDirectory.Authority,
PostLogoutRedirectUri = configuration.AzureActiveDirectory.PostLogoutRedirectUri,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = this.OnRedirectToIdentityProvider,
OnRemoteFailure = this.OnRemoteFailure,
OnTokenValidated = this.OnTokenValidated,
OnAuthorizationCodeReceived = this.OnAuthorizationCodeReceived,
OnAuthenticationFailed = this.OnAuthenticationFailed
}
};
}
}
Here is my DistributedCacheTicketStore:
public class DistributedCacheTicketStore : ITicketStore
{
private readonly DistributedCacheTicketStoreOptions options;
private readonly IDistributedCache distributedCache;
private readonly IDataProtector dataProtector;
private readonly ILogger<DistributedCacheTicketStore> logger;
public DistributedCacheTicketStore(
IOptions<DistributedCacheTicketStoreOptions> optionsAccessor,
IDistributedCache distributedCache,
IDataProtectionProvider dataProtectionProvider,
ILogger<DistributedCacheTicketStore> logger)
{
this.options = optionsAccessor.Value;
this.distributedCache = distributedCache;
this.dataProtector = dataProtectionProvider.CreateProtector(this.GetType().FullName);
this.logger = logger;
}
public async Task<string> StoreAsync(AuthenticationTicket ticket)
{
var key = Guid.NewGuid().ToString();
var ticketBytes = this.dataProtector.Protect(TicketSerializer.Default.Serialize(ticket));
await this.distributedCache.SetAsync(key, ticketBytes, new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(this.options.Session.TimeoutMinutes) });
this.logger.AuthenticationTicketStoredInCache(key);
return key;
}
public async Task RenewAsync(string key, AuthenticationTicket ticket)
{
var ticketBytes = this.dataProtector.Protect(TicketSerializer.Default.Serialize(ticket));
await this.distributedCache.SetAsync(key, ticketBytes, new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(this.options.Session.TimeoutMinutes) });
this.logger.AuthenticationTicketRenewedInCache(key);
}
public async Task<AuthenticationTicket> RetrieveAsync(string key)
{
var ticketBytes = await this.distributedCache.GetAsync(key);
var ticket = TicketSerializer.Default.Deserialize(this.dataProtector.Unprotect(ticketBytes));
this.logger.AuthenticationTicketRetrievedFromCache(key);
return ticket;
}
public async Task RemoveAsync(string key)
{
var ticketBytes = await this.distributedCache.GetStringAsync(key);
if (ticketBytes != null)
{
await this.distributedCache.RemoveAsync(key);
this.logger.AuthenticationTicketRemovedFromCache(key);
}
}
}
I have a WabApi project which uses Owin and NancyFX. The api is secured by OpenId Connect and cookie authentication.
I have to write some in memory integration tests using HttpClient, which is quite easy as long as you don't try to use authentication based on OpenId Connect and cookie.
Does anyone know how to prepare a proper authentication cookie for HttpClient to let it connect with WebApi as authenticated user?
Currently I'm able to do some http calls to get the proper access token, id token etc. from OpenId Connect provider (implemented by IdentityServer v3), but I have no idea how to prepare authentication cookie for HttpClient.
PS: I uses Hybrid flow for OpenId Connect
Below you can find some of my files.
Server project:
AppStartup for WebApi:
The server application hosts WebApi and OpenId Connect provider (IdentityServer v3) at the same time, so its app starup looks like this:
public class ServerAppStartup
{
public static void Configuration(IAppBuilder app)
{
app.Map("/identity", idsrvApp =>
{
var factory = new IdentityServerServiceFactory {...};
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "server app",
SigningCertificate = ...,
RequireSsl = false,
Factory = factory,
AuthenticationOptions = new AuthenticationOptions {
RememberLastUsername = true
},
EnableWelcomePage = false
});
});
app.SetDefaultSignInAsAuthenticationType("ClientCookie");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = "ClientCookie",
CookieName = CookieAuthenticationDefaults.CookiePrefix + "ClientCookie",
ExpireTimeSpan = TimeSpan.FromMinutes(5)
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(),
Authority = options.BaseUrl+ "identity",
ClientId = options.ClientId,
RedirectUri = options.RedirectUri,
PostLogoutRedirectUri = options.PostLogoutRedirectUri,
ResponseType = "code id_token",
Scope = "openid profile offline_access",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
/* stuff to get ACCESS TOKEN from CODE TOKEN */
},
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
}
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseNancy();
app.UseStageMarker(PipelineStage.MapHandler);
}
Sample nancy module (something like controller in MVC or WebApi):
using System;
using Nancy.ModelBinding;
using Nancy.Security;
namespace Server.Modules
{
public class UsersModule : BaseModule
{
public UsersModule() : base("/users")
{
Get["/getall"] = parameters =>
{
this.RequiresMSOwinAuthentication();
...
return ...;
};
}
}
}
Integration test project:
Test server to let me run WebApi in memory:
public class TestServer: IDisposable
{
private Func<IDictionary<string, object>, Task> _appFunc;
public static CookieContainer CookieContainer;
public Uri BaseAddress { get; set; }
// I uses OwinHttpMessageHandler becaouse it can handle http redirections
public OwinHttpMessageHandler Handler { get; private set; }
public HttpClient HttpClient => new HttpClient(Handler) { BaseAddress = BaseAddress };
public static TestServer Create()
{
CookieContainer = new CookieContainer();
var result = new TestServer();
var appBuilder = new AppBuilder();
appBuilder.Properties["host.AppName"] = "WebApi server";
/* Use configuration of server app */
ServerAppStartup.Configuration(appBuilder);
result._appFunc = appBuilder.Build();
result.Handler = new OwinHttpMessageHandler(result._appFunc)
{
AllowAutoRedirect = true,
AutoRedirectLimit = 1000,
CookieContainer = CookieContainer,
UseCookies = true
};
return result;
}
public void Dispose()
{
Handler.Dispose();
GC.SuppressFinalize(this);
}
}
Sample test:
namespace ServerSpec.Specs.Users
{
public class GetAllUsersSpec
{
private TestServer _server;
public GetAllUsersSpec(){
server = TestServer.create();
}
[Fact]
public void should_return_all_users()
{
/* here I will get error because http client or rather its cookie handler has no authentication cookie */
var users = Get("/users/getall");
...
}
public TResponse Get<TResponse>(string urlFragment)
{
var client = server.HttpClient();
var httpResponse = client.GetAsync(urlFragment).Result;
httpResponse.EnsureSuccessStatusCode();
return httpResponse.Content.ReadAsAsync<TResponse>().Result;
}
}
}
Check out the unit and integration tests in this project:
https://github.com/IdentityModel/IdentityModel.Owin.PopAuthentication
It shows doing in-memory integration testing with IdentityServer in one pipeline, and a (fake) web api in another pipeline that accepts tokens.