Integration Testing and MFA

dotnet
api

#1

My original post regarding integration testing is here: [Unit Testing and Implicit Flow]

This worked great when I was “in network”. The VSTS builds and releases run outside of the network and trigger MFA.

Is there a way I can answer the MFA questions programmatically using c# for integration testing?


#2

Hi @glenndorr ,

Not sure this will help specifically with your use case, but this page contains a video, a Postman collection, and instructions on how to consume/validate MFA via APIs.

https://developer.okta.com/use_cases/mfa/


#3

Thanks @frederico.hakamine,

I have read that article…That process allows you to verify the MFA for a user given an API token. I don’t think it addresses being able to respond to a MFA during integration testing. I can’t ask for an API token because including that in a build process would leak to much power.

I call authn and that returns a code that I can use to call authenticate and that in turn responses with an id_token in the redirect url IF you are in network for our sso enabled OKTA instance.

Because the process is running during a build, it has to be headless.


#4

@vijet or @lboyette or @jmelberg, any suggestions?


#5

@glenndorr: Since Okta APIs are not OAuth enabled, you’ll still have to use an API Token to verify a factor. I’d suggest exploring ways to encrypt the API Token in your build process and use that to call the factors API.

@robertjd, @jmelberg, @bdemers, @bretterer - Any thoughts?


#6

Hi @glenndorr, is the issue here that you need to respond to an MFA challenge during an authentication flow in an IT test?


#7

@robertjd Yes! That is correct


#8

In that case I would recommend using a TOTP factor for your test user in your test flow. In this case you only need to put the shared secret for the factor in your test environment, then you can use that shared secret to generate TOTP pass codes when challenged. I think you’re in .NET land? In node-land I’ve been using this library to generate codes https://github.com/speakeasyjs/speakeasy


#9

Thank you @robertjd.

So I call authn with userid and password. returns a challenge with this (obfuscated) content

{{
  "stateToken": "the state token",
  "expiresAt": "2018-03-06T22:49:02Z",
  "status": "MFA_REQUIRED",
  "_embedded": {
    "user": {
      "id": "the user id",
      "profile": {
        "login": "The login",
        "firstName": "first name",
        "lastName": "last name",
        "locale": "en",
        "timeZone": "America/Los_Angeles"
      }
    },
    "factors": [
      {
        "id": "factor id",
        "factorType": "question",
        "provider": "OKTA",
        "vendorName": "OKTA",
        "profile": {
          "question": "the question,
          "questionText": "the question text"
        },
        "_links": {
          "verify": {
            "href": "https://our sub domain.okta.com/api/v1/authn/factors/default/verify",
            "hints": {
              "allow": [
                "POST"
              ]
            }
          }
        }
      }
    ],
    "policy": {
      "allowRememberDevice": true,
      "rememberDeviceLifetimeInMinutes": 120,
      "rememberDeviceByDefault": false
    }
  },
  "_links": {
    "cancel": {
      "href": "https://our sub domain.okta.com/api/v1/authn/cancel",
      "hints": {
        "allow": [
          "POST"
        ]
      }
    }
  }
}}

what are you suggesting next?


#10

Solved it! I’ll clean up the code and post something on Monday. Maybe you guys can reformat it and put up a blog on it.


#11

@glenndorr : I am stuck with the exact issue. It would be helpful if you could share what exactly you did to solve it


#12

I know this is pretty raw but it should get you past the challenge of MFA.

The model I use:

public class OktaAuthRequestInformation
{
    public string Domain { get; set; }
    public string OktaAuthorizationServer { get; set; }
    public string ClientId { get; set; }
    public string RedirectUrl { get; set; }
    public string RedirectUrlEncoded => System.Net.WebUtility.UrlEncode(RedirectUrl);
    public string ResponseType { get; set; } = System.Net.WebUtility.UrlEncode("id_token");
    public string State { get; set; } = Guid.NewGuid().ToString();
    public string Nonce { get; set; } = Guid.NewGuid().ToString();
    public string Scope { get; set; } = System.Net.WebUtility.UrlEncode("openid email profile");
    public string AuthnUri => $"{Domain}/api/v1/authn";
    
    public string AuthorizeUri => $"{Domain}/oauth2/{OktaAuthorizationServer}/v1/authorize";
    public string Username { get; set; }
    public string Password { get; set; }
    public string MultiFactorAuthenticationQuestionAnswer { get; set; }
}

The class the communicates with OKTA.

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Web;

public static class OktaRequests
{
    private const string _mediaType = "application/json";
    private const string _idTokenKey = "id_token";
    private const string _accessTokenKey = "access_token";

    public static async Task<string> GetOktaToken(string domain, string authServer, string clientId, string redirectUrl, string userId, string password, string multiFactorAuthenticationQuestionAnswer)
    {
        var oktaAuthRequestInformation = new OktaAuthRequestInformation {
            Domain = domain,
            OktaAuthorizationServer = authServer,
            ClientId = clientId,
            RedirectUrl = redirectUrl,
            Username = userId,
            Password = password,
            MultiFactorAuthenticationQuestionAnswer = multiFactorAuthenticationQuestionAnswer
        };

        StringContent stringContent = GetContentForOktaAuthNCall(oktaAuthRequestInformation);

        HttpClientHandler httpClientHandler = new HttpClientHandler
        {
            AllowAutoRedirect = false
        };

        using (var httpClient = new HttpClient(httpClientHandler))
        {
            httpClient.DefaultRequestHeaders
                .Accept
                .Add(new MediaTypeWithQualityHeaderValue(_mediaType));

            var result = await CallOKTAAuthN(httpClient, oktaAuthRequestInformation, stringContent);
            return result[_idTokenKey];
        }
    }

    public static async Task<Dictionary<string, string>> GetOktaIdTokenAndAccessToken(string domain, string authServer, string clientId, string redirectUrl, string userId, string password, string multiFactorAuthenticationQuestionAnswer)
    {
        var oktaAuthRequestInformation = new OktaAuthRequestInformation
        {
            Domain = domain,
            OktaAuthorizationServer = authServer,
            ClientId = clientId,
            RedirectUrl = redirectUrl,
            Username = userId,
            Password = password,
            MultiFactorAuthenticationQuestionAnswer = multiFactorAuthenticationQuestionAnswer
        };

        oktaAuthRequestInformation.ResponseType = System.Net.WebUtility.UrlEncode("token id_token");
        
        StringContent stringContent = GetContentForOktaAuthNCall(oktaAuthRequestInformation);

        HttpClientHandler httpClientHandler = new HttpClientHandler
        {
            AllowAutoRedirect = false
        };

        using (var httpClient = new HttpClient(httpClientHandler))
        {
            httpClient.DefaultRequestHeaders
                .Accept
                .Add(new MediaTypeWithQualityHeaderValue(_mediaType));

            return await CallOKTAAuthN(httpClient, oktaAuthRequestInformation, stringContent);
        }
    }

    private static StringContent GetContentForOktaAuthNCall(OktaAuthRequestInformation oktaAuthRequestInformation)
    {
        dynamic bodyOfRequest = new
        {
            username = oktaAuthRequestInformation.Username,
            password = oktaAuthRequestInformation.Password,
            options = new
            {
                multiOptionalFactorEnroll = true,
                warnBeforePasswordExpired = true
            }
        };

        var body = JsonConvert.SerializeObject(bodyOfRequest);

        var stringContent = new StringContent(body, Encoding.UTF8, _mediaType);
        return stringContent;
    }

    private static async Task<Dictionary<string, string>> CallOKTAAuthN(HttpClient httpClient, OktaAuthRequestInformation oktaAuthRequestInformation, StringContent stringContent)
    {
        HttpResponseMessage authnResponse = await httpClient.PostAsync(oktaAuthRequestInformation.AuthnUri, stringContent);

        if (authnResponse.StatusCode == HttpStatusCode.Unauthorized)
        {
            throw new UnauthorizedAccessException("The user could not be authenticated.");
        }

        if (authnResponse.IsSuccessStatusCode)
        {
            var responseContent = await authnResponse.Content.ReadAsStringAsync();
            var response = JsonConvert.DeserializeObject<OktaMfaResponse.ResultOfACall>(responseContent);

            if(response.status == "MFA_REQUIRED")
            {
                var mfaResponse = JsonConvert.DeserializeObject<OktaMfaResponse.MfaRequiredResult>(responseContent);
                responseContent = await CallOktaPasscodeVerifyForMFA(httpClient, mfaResponse._embedded.factors[0]._links.verify.href, mfaResponse.stateToken, oktaAuthRequestInformation.MultiFactorAuthenticationQuestionAnswer);

                response = JsonConvert.DeserializeObject<OktaMfaResponse.ResultOfACall>(responseContent);
            }

            if (response.status != "SUCCESS")
            {
                throw new UnauthorizedAccessException($"Can't authenticate with OIDC {responseContent}");
            }

            var successfulResponse = JsonConvert.DeserializeObject<OktaMfaResponse.SuccessResult>(responseContent);

            return await CallOktaAuthorize(httpClient, oktaAuthRequestInformation, successfulResponse.sessionToken);
        }
        throw new InvalidOperationException($"Something went wrong in {nameof(CallOKTAAuthN)}");
    }

    private static async Task<string> CallOktaPasscodeVerifyForMFA(HttpClient httpClient, string url, string stateToken, string multiFactorAuthenticationQuestionAnswer)
    {
        var payload = new
        {
            stateToken,
            answer = multiFactorAuthenticationQuestionAnswer
        };
        var serialized = JsonConvert.SerializeObject(payload);
        var body = new StringContent(serialized, Encoding.UTF8, _mediaType);

        HttpResponseMessage authorizeResponse = await httpClient.PostAsync(url, body);
        var responseContent = await authorizeResponse.Content.ReadAsStringAsync();

        if (!authorizeResponse.IsSuccessStatusCode)
        {
            throw new UnauthorizedAccessException($"Can't authenticate with OIDC {responseContent}");
        }

        return responseContent;
    }

    private static async Task<Dictionary<string, string>> CallOktaAuthorize(HttpClient httpClient, OktaAuthRequestInformation oktaAuthRequestInformation, string sessionToken)
    {
        var authorizeUri = oktaAuthRequestInformation.AuthorizeUri + "?" + 
            $"client_id={oktaAuthRequestInformation.ClientId}" +
            $"&redirect_uri={oktaAuthRequestInformation.RedirectUrlEncoded}" +
            $"&response_type={oktaAuthRequestInformation.ResponseType}" +
            $"&sessionToken={sessionToken}" +
            $"&state={oktaAuthRequestInformation.State}" +
            $"&nonce={oktaAuthRequestInformation.Nonce}" +
            $"&scope={oktaAuthRequestInformation.Scope}";

        HttpResponseMessage authorizeResponse = await httpClient.GetAsync(authorizeUri);
        var statusCode = authorizeResponse.StatusCode;

        if(statusCode == System.Net.HttpStatusCode.Found)
        {
            var redirectUri = authorizeResponse.Headers.Location;
            var queryDictionary = HttpUtility.ParseQueryString(redirectUri.AbsoluteUri);
            var idTokenKey = $"{oktaAuthRequestInformation.RedirectUrl}#{_idTokenKey}";

            var idToken = queryDictionary[idTokenKey];

            if (string.IsNullOrWhiteSpace(idToken))
            {
                throw new InvalidOperationException($"Something went wrong in {nameof(CallOktaAuthorize)}, redirect of {redirectUri}");
            }

            var result = new Dictionary<string, string>();
            result.Add(_idTokenKey, idToken);

            var accessToken = queryDictionary[_accessTokenKey];
            if (accessToken != null)
            {
                result.Add(_accessTokenKey, accessToken);
            }
            return result;
        }

        throw new InvalidOperationException($"Something went wrong in {nameof(CallOktaAuthorize)}");
    }
}

#13

Thanks for the response. It helped me understand the flow. My Org’s Okta setup may not allow any static factors like ‘question’.