OAuth for Okta - Users outside my org

(I had opened an issue on the @okta/okta-auth-js repo, and got great help, but this felt like more of a Question than an issue for that project, so moving here)

I would like to create an integration where any Okta admin, belonging to any org, can OAuth their account, providing my application with an access_token that can be used to manage apps and users in the authenticated user’s organization.

The ultimate goal is to create a flow where an Okta org admin can authenticate our application, and then we use the REST API to create a custom SAML application, assign it to the authed user, and fetch the XML metadata, which we persist on our side to enable SAML authentication. This would allow our Okta customers to enable SAML SSO for our application without having to go through the process of creating the custom SAML 2.0 app via the “Classic UI”.

I’ve been following the OAuth for Okta guides, which say

With OAuth for Okta, you are able to interact with Okta APIs using scoped OAuth 2.0 access tokens. Each access token enables the bearer to perform specific actions on specific Okta endpoints, with that ability controlled by which scopes the access token contains.

What’s a bit unclear is whether this can apply to Okta users outside of my organization? I’ve got the whole flow working, but it’s against an OAuth app in my specific instance. If a user from another org tries to log in, they cannot.

I’ve been reading up on authorization servers, and I am currently using our organization’s Org Authorization Server. There’s also the Default Custom Authorization Server, but that feels like it moves in the wrong direction from what I need. Again, from the docs:

Only the Org Authorization Server can mint access tokens that contain Okta API scopes.

But this of course only applies to users in my org - I even have to assign them the OAuth app before they can OAuth.

Is there a cross-org authorization server? Am I going about this wrong?

Here’s what I’ve hacked together so far, which honestly works great for a user in my org who has been assigned the app with CLIENT_ID, but it obviously doesn’t work for users in other orgs.

We’re open source, so happy to share this, and can get it up in a PR soon if helpful to Okta team.

export default function OktaOAuth({ identityProvider }: { identityProvider?: IdentityProvider }): ReactElement {
  const authClient = new OktaAuth({
    pkce: true,
    clientId: CLIENT_ID,
    issuer: 'https://dev-#####.okta.com',
    redirectUri: 'http://localhost:3000/implicit/callback',
    scopes: ['openid', 'profile', 'email', 'okta.users.manage', 'okta.apps.manage'],
  });

  return (
    <>
      <a
        onClick={() => {
          authClient?.token
            .getWithPopup()
            .then(function (res: any) {
              console.log(res);
              // now we can call a mutation with res.tokens.accessToken
              // to create and configure the application
            })
            .catch(function (err: any) {
              console.error(err);
              // handle OAuthError or AuthSdkError (AuthSdkError will be thrown if app is in OAuthCallback state)
            });
        }}
      >
        Sign in with Okta
      </a>
    </>
  );
}

Then there’s a ruby class that does the app creation part:

# frozen_string_literal: true

require 'oktakit'
require 'ruby-saml'

module Osso
  class OktaConfiguration
    attr_accessor :client, :identity_provider

    def self.perform(args)
      new(**args).perform
    end

    def initialize(access_token:, identity_provider:)
      @identity_provider = identity_provider
      @client = Oktakit.new(access_token: access_token, organization: 'dev-#####')
    end

    def perform
      app = create_app
      assign_user(app)
      configure_identity_provider(app)
    end

    private

    def configure_identity_provider(app)
      metadata, status = client.preview_saml_metadata_for_application(app[:id], { content_type: 'application/xml', accept: 'application/xml'})
      idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
      settings = idp_metadata_parser.parse_to_hash(metadata)
      identity_provider.update(
        sso_url: settings[:idp_sso_target_url],
        sso_cert: settings[:idp_cert]
      )
    end

    def assign_user(app)
      user = client.get_user('me')&.first
      client.assign_user_to_application_for_sso(app[:id], { id: user[:id]})
    end

    def create_app
      response = client.add_application(
        label: 'Custom SAML via API',
        signOnMode: 'SAML_2_0',
        "visibility": {
          "autoSubmitToolbar": false,
          "hide": {
            "iOS": false,
            "web": false
          }
        },
        "settings": {
          "signOn": {
            "defaultRelayState": "",
            "ssoAcsUrl": identity_provider.acs_url,
            "idpIssuer": "http://www.okta.com/${org.externalKey}",
            "audience": identity_provider.sso_issuer,
            "recipient": identity_provider.acs_url,
            "destination": identity_provider.acs_url,
            "subjectNameIdTemplate": "${user.userName}",
            "subjectNameIdFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
            "responseSigned": true,
            "assertionSigned": true,
            "signatureAlgorithm": "RSA_SHA256",
            "digestAlgorithm": "SHA256",
            "honorForceAuthn": true,
            "authnContextClassRef": "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
            "spIssuer": nil,
            "requestCompressed": false,
            "attributeStatements": [
                {
                    "type": "EXPRESSION",
                    "name": "id",
                    "namespace": "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified",
                    "values": [
                        "user.id"
                    ]
                },
                {
                    "type": "EXPRESSION",
                    "name": "email",
                    "namespace": "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified",
                    "values": [
                        "user.email"
                    ]
                }
            ],
            "allowMultipleAcsEndpoints": false,
            "acsEndpoints": []
          }
        }
      )
      Array(response).flatten.first # returns an array for some reason

      rescue => e
        puts e
    end
  end
end