We would like to replace our web-based login page (which calls the ServiceStack auth service against a CredentialsAuthProvider) with the Okta sign-in widget. This will allow us to plug in two-factor authentication.
My web developer got the Okta sign-in widget working, and it returns a JSON object with a userid and a token - I am not sure how to create the correct ServiceStack auth code that parses the token, checks for validity, and then authenticates the user to the service.
You will need to create a Custom AuthProvider. Have a look at one of the other OAuth Providers for examples which handle OAuth flow redirects and sets up a valid Auth User Session if they’re called back with a valid Access Token. The implementations for most Auth Providers are available in the /ServiceStack/auth folder.
The alternative way to Authenticate is directly with a user-specified AccessToken which follows a similar code-path but skips the OAuth redirect flow used to retrieve the AccessToken and just authenticates directly.
Not sure what your Okta Custom AuthProvider will eventually end up looking like, but it needs to eventually call base.OnAuthenticated() and implement LoadUserAuthInfo() to populate the Users Session.
Got the basic concept above working - one remaining question - can my services have more than one subclass of CredentialsAuthProvider that is called based on the “provider” in the Authenticate object? It turns out that I’m not using OAuth, and the structure of the CredentialsAuthProvider will work well for my needs, but I already have one that will still be used in some cases.
I tried overridding the lower level ancestor AuthProvider but haven’t gotten it wired up correctly yet (LoadUserAuthInfo is not getting called).
You’ll need to specify a different Provider name for the CredentialsAuthProvider, only one of them can be called “credentials” and handle the default /auth/credentials requests.
The basics of my class are attached . The ConfigInfo class that is referenced is a bunch of statics out of my config file. The DVUser class is saved at the end and then reused in OnAuthenticated to fill the session properties.
Disclaimer: no idea if this is correct. I am now using something different that inherits of OAuthProvider, but I haven’t tested to see if it works with the Okta signin widget yet. My Okta stuff has yet to be used in production.
using System;
using System.Text;
using System.Collections.Generic;
using ServiceStack;
using ServiceStack.Text;
using ServiceStack.Configuration;
using ServiceStack.Auth;
using ServiceStack.Web;
//this was used by the login widget
namespace DV.Svc.Interface
{
public class OktaAuthProvider : CredentialsAuthProvider
{
private DVUser usr;
//how can I do these in order without having to repeat the string
//there's a "Name" static but I don't think I can get to it
public OktaAuthProvider(IAppSettings appSettings) : base(appSettings, Realm, "Okta")
{
Name = "Okta";
}
public override object Authenticate(IServiceBase authService, IAuthSession session, Authenticate request)
{
bool bDenyAccess = false;
//makes sure the fields coming from Okta match our values
if (request.Meta.Get("iss") != ConfigInfo.OktaSite)
bDenyAccess = true;
if (request.Meta.Get("aud") != ConfigInfo.OktaClientId1)
bDenyAccess = true;
//parse the jwt token (middle section)
string[] jwts = request.oauth_token.Split('.');
if (jwts.Length != 3)
{
bDenyAccess = true;
}
else
{
var token = new StringBuilder(jwts[1]);
while (token.Length % 4 > 0)
token.Append("=");
byte[] buffer = Convert.FromBase64String(token.ToString());
string json = Encoding.UTF8.GetString(buffer);
dynamic dyn = DynamicJson.Deserialize(json);
//fields inside the token should match also
if (dyn.iss != ConfigInfo.OktaSite)
bDenyAccess = true;
if (dyn.aud != ConfigInfo.OktaClientId1)
bDenyAccess = true;
}
usr = new DVUser()
{
UserID = request.UserName,
Lastname = request.Meta.Get("family_name"),
Firstname = request.Meta.Get("given_name"),
Email = request.Meta.Get("email"),
DenyAccess = bDenyAccess
};
request.Password = "xxx"; //dummy
return base.Authenticate(authService, session, request);
}
public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
{
return !(usr.DenyAccess); //returns true if authenticated
}
public override IHttpResult OnAuthenticated(IServiceBase authService, IAuthSession session, IAuthTokens tokens, Dictionary<string, string> authInfo)
{
session.IsAuthenticated = true;
session.UserName = usr.UserID;
session.UserAuthName = usr.UserID;
session.FirstName = usr.Firstname;
session.LastName = usr.Lastname;
session.DisplayName = usr.Fullname;
//fill other session stuff here (maybe from database?)
//save this here, needed when system re-saves session
((DVAuthUserSession)session).Expiry = this.SessionExpiry;
return base.OnAuthenticated(authService, session, tokens, authInfo);
}
}
}