1
votes

Trying to create my own custom OpenId Auth provider, which will point to an IdentityServer service, but can't seem to find OpenIdOAuthProvider in the ServiceStack assembly.

VS 2017 says

Error CS0012 The type 'OAuthProvider' is defined in an assembly that is not referenced. You must add a reference to assembly 'ServiceStack, Version=5.0.0.0, Culture=neutral, PublicKeyToken=02c12cbda47e6587'

public class MyCustomOpenIdOAuthProvider : ServiceStack.Authentication.OpenId.OpenIdOAuthProvider
    {
        //code goes here to point to IdentityServer Realm...
    }

I do have ServiceStack 5.4.1 and ServiceStack.Authentication.OpenId referenced of course. .Net Core 2.2

Any help appreciated here.

1

1 Answers

1
votes

The ServiceStack.Authentication.OpenId package is .NET Framework only project that's not supported in .NET Core due to OpenIdOAuthProvider.cs requiring DotNetOpenAuth which is not available for .NET Core.

As you're using the v5.4.1 pre-release MyGet packages you may be interested in the new NetCoreIdentityAuthProvider which provides an adapter which maps an Authenticated .NET Core Identity user to an authenticated ServiceStack User Session.

So if you authenticate your .NET Core App to with IdentityServer then the authenticated user should map to an Authenticated UserSession when accessing ServiceStack Services.

SS API Example

I have a few notes after looking into your example on GitHub. Basically since you've decided to use IdentityServer as Authentication Provider you remove all other ServiceStack AuthProviders except for NetCoreIdentityAuthProvider:

Plugins.Add(new AuthFeature(() => new AuthUserSession(),
    new IAuthProvider[] {
        new NetCoreIdentityAuthProvider(AppSettings), 
  }));

Which provides the adapter to convert from an Identity User ClaimsPrincipal to a ServiceStack Authenticated UserSession (and vice-versa).

If you're using NetCoreIdentityAuthProvider you should also remove the alternative IdentityServerAuthFeature implementation as well so you're only adopting a single solution to integrated with Identity Server Auth:

// this.Plugins.Add(new IdentityServerAuthFeature() // remove...

As the Identity Server uses JWT to populate the ClaimsPrincipal User it doesn't use ClaimTypes.NameIdentifier to indicate what to use for the Session Id. JWT's typically have a "sub" Claim to identify the principal that is the subject of the JWT which is populated in ServiceStack's AuthUserSession.Id and the AuthUserSession.Type will be populated with "sub" to indicate the Type of Authenticated Session and that the JWT's sub (typically the Users Id) is used in the Session Id.

You'll be able to use the default NetCoreIdentityAuthProvider configuration to authenticate with Identity Server JWTs that authenticates a User as done in your first call in RequestTokenAsync_CallResourceOwnerFlow():

var response = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
{
    Address = disco.TokenEndpoint,

    ClientId = "roclient",
    ClientSecret = "secret",

    UserName = "bob",
    Password = "password",

    Scope = "apiMB openid",
});

Which you can verify by decoding the JWT in jwt.io, here's the payload of an example token I got from this request:

{
  "nbf": 1545170425,
  "exp": 1545174025,
  "iss": "http://127.0.0.1:65048",
  "aud": [
    "http://127.0.0.1:65048/resources",
    "apiMB"
  ],
  "client_id": "roclient",
  "sub": "2",
  "auth_time": 1545170425,
  "idp": "local",
  "scope": [
    "openid",
    "apiMB"
  ],
  "amr": [
    "pwd"
  ]
}

Which we can see contains the "sub" of "2" which is Bob's UserId:

new TestUser
{
    SubjectId = "2",
    Username = "bob",
    Password = "password",
    IsActive = true
}

Your 2nd Authentication Request is an example of authenticating a client:

var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
    Address = disco.TokenEndpoint,

    ClientId = "client",
    ClientSecret = "secret",
    Scope = "apiMB"
});

Which doesn't contain a subject in its JWT but instead is only populated with:

{
  "nbf": 1545170914,
  "exp": 1545174514,
  "iss": "http://127.0.0.1:65048",
  "aud": [
    "http://127.0.0.1:65048/resources",
    "apiMB"
  ],
  "client_id": "client",
  "scope": [
    "apiMB"
  ]
}

In this case ServiceStack with authenticate it as a client where AuthUserSession.Type will be populated with client_id and the session Id will be populated with client.

By default ServiceStack allows authenticating with any Authenticated client_id (which is validated by Identity Server before it reaches ServiceStack). If you instead only want to restrict a subset of client applications to be able to access ServiceStack Services you can specify them in RestrictToClientIds, e.g:

Plugins.Add(new AuthFeature(() => new AuthUserSession(),
    new IAuthProvider[] {
        new NetCoreIdentityAuthProvider(AppSettings) {
            RestrictToClientIds = new List<string> { "client" },
        }, 
  }));

Where ServiceStack will only allow access from client applications with the client client_id.

The Audiences (aud) of the JWT are populated in AuthUserSession's Audiences whilst the JWT scopes are populated in Scopes property, whilst all other un-mached properties from the JWT in the sessions Meta dictionary.

These changes in NetCoreIdentityAuthProvider are only available in the latest v5.4.1 on MyGet. If you already had v5.4.1 you'll need to clear your NuGet cache to fetch the latest v5.4.1 from MyGet:

$ nuget locals all -clear

Code Review Issues

Not related to Authentication but I noticed a few issues in your code where to read the AppSettings from your App you should just use the AppSettings property instead of creating a new instance:

// var appSettings = new AppSettings(); // Use base.AppSettings instead

You can also access .NET Core's IConfiguration with:

var configuration = ((NetCoreAppSettings) AppSettings).Configuration;

Instead of trying to re-create it with:

//IConfigurationRoot configuration = new ConfigurationBuilder()
//       .SetBasePath(Directory.GetCurrentDirectory())
//       .AddJsonFile("appsettings.json")
//       .Build();