IdentityServer V3 und eigene Dependency Injection mit OWIN

Der Identity Server v3 ist ein OAuth2 und OpenID Connect Framework welches einem die Serverseitige Implementierung von OAuth2 und OpenID Connect abnimmt. Im Gegensatz zu vorheringen Versionen steht hier das konzept klar auf Erweiterbarkeit und weg vom Monolitischen Ansatz wo Server und Administration vereint sind.

Der IdServerV3 ist eine reine ASP.NET Web API Lösung, und benutzt intern Autofac um selbst ordentlich mit Dependency Injection und IoC arbeiten zu können. Dies stellt jedoch eine kleine Herausforderung dar dies in der bestehenden Anwendung die schon mit IoC arbeitet zu verwenden, da IdServerV3 es nicht erlaubt den bestehenden IoC Container einfach einzustöpseln. Da es ohne Aufwändige IoC Konfiguration laufen soll kann man diese Entscheidung auch nachvollziehen.

TL;DR

Wie schaffe ich es nun mit einem eigenen IoC zu arbeiten?

Füttern des IoC Containers von IdServerV3

Es gibt im IdServerV3 die IdentityServerServiceFactory, damit kann man die Services die man für den Identity Server selber implementieren will oder muss (weil man z.B. eine eigene Administration und Datenhaltung hat).

var factory = new IdentityServerServiceFactory
{
    UserService = new Registration<IUserService,IdentityServerUserService>(),
    ClientStore = new Registration<IClientStore, IdentityServerClientStore>(),
    ScopeStore = new Registration<IScopeStore,IdentityServerScopeStore>()
};

Hiermit registriert man den UserService, ClientStore und ScopeStore, diese braucht der IdServerV3 auf jedenfall, für den Produktionsbetrieb noch ein paar weitere.

Jedoch wird bei der Service Erstellung nun der interne Autofac Container verwendet. Und nicht der IoC Container der Applikation. Damit die funktioniert kann mann über nun die eigenen Services auch dem internen Container bekannt machen. Und natürlich auch dessen abhängikeiten.

Dazu kann man.

factory.Register(new Registration<IFoo,Foo>())

verwenden. Dies ist jedoch nicht zu empfehlen, da man so sehr viele eigene Services auch noch im IdServerV3 Container registrieren muss. Jedoch viel wichtiger, weil so das LifeCycle Managment des IoC Containers ausgehebelt wird und man nun effektiv 2 IoC in der Anwendung hat und jeder seinen eigenes LifeCycle Managment hat und eigene Instanzen erzeugt. Dies führt über kurz oder lang zu Problemen.

Lösungansatz

Ein mögliche Lösung ist nun eine Factory Methode bei der Registrierung anzugeben, dies wird dann vom IdServerV3 Containers bei der Erzeugung des Services aufgerufen.

Variante 1 (so nicht verwenden)

var factory = new IdentityServerServiceFactory
{
    UserService = new Registration<IUserService>(dr => mycontainer.GetInstance<IUserService>()),
    ClientStore = new Registration<IClientStore>(dr =>  mycontainer.GetInstance<IClientStore>()),
    ScopeStore = new Registration<IScopeStore>(dr =>  mycontainer.GetInstance<IScopeStore>())
};	

Dies ist prinzipiell funktionsfähig, jedoch arbeitet man hier mit einer bestimmten Instanz des Container, und war immer mit derselben. Was wieder dem LifeCycle Managment wiederspricht da so nicht der Scoped-Container von WebAPI verwendet wird. Und somit im Zweifel nicht jeder Request seine eigenen Instanzen hat und diese auch nicht beim Ende des Request wieder Disposed werden. Also nicht nachmachen.

Variante 2

Es ist notwendig für die Request die zum IdentityServer gehen einen eigenen Scope des IoC Containers aufzumachen und wieder zuschließen um dann in der Registrierung Methode darauf zuzugreifen. Da jedoch IdServerV3 selbst einen IoC Container verwendet, sollte man dies in einer Form machen die damit nicht zusammstößt.

Eine einfache IdServer-OWIN Konfiguration

app.Map("/identity", idsrvApp =>
{
    var factory = new IdentityServerServiceFactory
    {
        UserService = new Registration<IUserService,IdentityServerUserService>(),
        ClientStore = new Registration<IClientStore, IdentityServerClientStore>(),
        ScopeStore = new Registration<IScopeStore,IdentityServerScopeStore>()
    };
    idsrvApp.UseIdentityServer(new IdentityServerOptions
    {
        SiteName = "Embedded IdentityServer",
        SigningCertificate = LoadCertificate(container),
        Factory = factory,
    });
});	

Ziel ist es nun in der Registrierung-Methode mit dem Applikations spezifischen Scoped-Container zu bestücken.

Dazu müssen wir in ersteinmal pro Request einen eigenen Scoped-Container haben, dazu erstellen wir eine kleine OWIN Middleware. Damit dies jetzt nicht IoC-Container spezifisch ist, nehmen wir den IDependencyResolver der schon in der WebAPI Verwendung findet.

internal class DependencyResolverMiddleWare
{
    readonly private Func<IDictionary<string, object>, Task> m_next;
    readonly IDependencyResolver m_resolver;

    public DependencyResolverMiddleWare(Func<IDictionary<string, object>, Task> next, IDependencyResolver resolver)
    {
        m_next = next;
        m_resolver = resolver;
    }

    public async Task Invoke(IDictionary<string, object> env)
    {
        var context = new OwinContext(env);
        using (var scope = m_resolver.BeginScope())
        {
            context.Request.Set("ioc.dependencyScope", scope);
            await m_next(env);
        }
    }
}

Im Endeffekt rufen wir beim Invoke den DependencyResolver.BeginScope um den Scoped Container zu erzeugen, speichern ihn zwischen, rufen den nächsten Schritt in der Pipeline auf (am besten IdentityServer) und disposen den Scope wenn der zurück kommt.

Dieser muss nur noch in der Pipeline vor dem IdentityServer registriert werden. Dann können wir auf den zwischengespeicherten Scope in der Registration-Methode darauf zugreifen.

app.Map("/identity", idsrvApp =>
{
    // ...
    idsrvApp.Use<DependencyResolverMiddleWare>(container.GetInstance<System.Web.Http.Dependencies.IDependencyResolver>());
    idsrvApp.UseIdentityServer(/*..*/);
});

Jetzt muss man nur noch an den Request spezfischen Scope kommen. Dazu muss man an das OwinEnviroment kommen, das IDictionary<string, object> wo alles drin steht was man braucht.

Diesen kann man sich nun über den IdentityServer eigenen IDependencyResolver holen, der wird praktischerweise der Registration-Methode übergeben.

var owin = (OwinEnvironmentService)resolver.GetService(typeof(OwinEnvironmentService));
var value = owin.Environment["keytoget"];

Daraus könnte man sich nun direkt den Scope-Container rauspulen, jedoch muss man sich dann die die Keys wühlen und da erstmal zum Request und weiter. Zum Glück gibt es Hilfsklassen die einem den typisierten Zugriff darauf erlauben.

var owin = resolver.Resolve<OwinEnvironmentService>();
var context = new OwinContext(owin.Environment);
var scope = context.Request.Get<System.Web.Http.Dependencies.IDependencyScope>("ioc.dependencyScope");

und schon ist man im Besitz des Scoped-Containers der in der Middleware erzeugt worden ist.

Zwecks einfacher Verwendung kann man dies nun schön in einer Extensionmethod unterbringen.

public static T GetInstanceFromApplicationDependencyResolver<T>(this Thinktecture.IdentityServer.Core.Services.IDependencyResolver resolver)
{
    var owin = resolver.Resolve<OwinEnvironmentService>();
    var context = new OwinContext(owin.Environment);
    var scope = context.Request.Get<System.Web.Http.Dependencies.IDependencyScope>("ioc.dependencyScope");
    return (T)scope.GetService(typeof(T));
}

so kann man dies dann einfach verwenden.

app.Map("/identity", idsrvApp =>
{
    var factory = new IdentityServerServiceFactory
    {
        UserService = new Registration<IUserService>(dr => dr.GetInstanceFromApplicationDependencyResolver<IUserService>()),
        ClientStore = new Registration<IClientStore>(dr => dr.GetInstanceFromApplicationDependencyResolver<IClientStore>()),
        ScopeStore = new Registration<IScopeStore>(dr => dr.GetInstanceFromApplicationDependencyResolver<IScopeStore>())
    };
    idsrvApp.Use<DependencyResolverMiddleWare>(container.GetInstance<System.Web.Http.Dependencies.IDependencyResolver>());
    idsrvApp.UseIdentityServer(new IdentityServerOptions
    {
        SiteName = "Embedded IdentityServer",
        SigningCertificate = LoadCertificate(),
        Factory = factory
    });
});

Natürlich müssen die Services und auch der IDependenyResolver im Applikations Container vorhanden sein, aber dies versteht sich von selbst.

Variante 3

Was meinst Du?