#Getting current user accessToken in a custom action

39 messages · Page 1 of 1 (latest)

vital stump
#

In a template action I am implementing I need to retrieve a OnBehalfOf token from Azure (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.onbehalfofcredential?view=azure-dotnet) to be able to communicate with a 3rd party API
to do so I need the current user (the user that triggered the action) access token. I am also using Azure for authentication in backstage

Enables authentication to Microsoft Entra ID using an On-Behalf-Of flow.

vital stump
#

I tried using ctx.secrets?.backstageToken but after looking at it with jwt.io I can see it's a ES256 token and I think azure is expecting a RS256 token so I am getting this error

AuthenticationRequiredError: invalid_request: 5002730 - [2024-01-23 21:24:08Z]: AADSTS5002730: Invalid JWT token. Unsupported key for the signing algorithm. Trace ID: xxxxxxxxx Correlation ID: xxxxxxxxx Timestamp: 2024-01-23 21:24:08Z - Correlation ID: xxxxxxxxx - Trace ID: xxxxxxxxx

vital stump
drifting umbra
#

the canonical way of doing things like this is to explicitly leverage eg microsoftAuthApiRef to get an access token that you send along as a secret

#

the tokens involved in the initial login exchange aren't necessarily directly available

vital stump
#

do you have an example of this (even with auth another provider)

drifting umbra
#

i think @fiery marsh knows more about what to best point to here

vital stump
#

so with this approach the end user would have to re-enter his user/password as part of the template action form ?

fiery marsh
#

@vital stump no, the idea is that the field extension will use the microsoftAuthApiRef to get a token, and if they're already authenticated they won't get a username and password popup. You don't have to render any UI component of course, it can just be a hidden component that mounts in the form that collects this secret and adds it to secrets using setSecret from useTemplateSecrets hook

#

We do something similar already in the RepoUrlPicker

#

That could be an option for you to use if you're looking for permissions for a repository

vital stump
#

ok so this could be a new field type that I have to implement
would I be able to hide it in the scaffolder form ? I don't want the user to enter anything additional, just grab his oauth token

fiery marsh
#

Yeah you can do that for sure. You can just add it to the parameters block and use ui:field to reference the new field that you've created

#

That's gonna render it in the form, but nothing is required.

#

You can of course also use the RepoUrlPicker if you want to collect repo information, it will automatically decorate a token using the microsoftAuthApiRef I think. But if you just want the token and no input from the user, then you're better to write you own.

vital stump
#

i'll try this out, thanks for the input

vital stump
#

ok I think I am quite close

this is my custom field

export const MicrosotOauthTokenProvider = ({
  name,

}: FieldProps<string>) => {
  const microsoftAuthApi= useApi(microsoftAuthApiRef);
  const { setSecrets, secrets } = useTemplateSecrets();
  useDebounce( async() => {
    const accessToken = await microsoftAuthApi.getAccessToken();
    setSecrets({
      [name]: accessToken
    });
    console.log(secrets);
  });
  return (<FormControl/>);
};

and I can see the expected access token under the expected key (I use the name of the field) in secrets (in the console log)

from the console log

{msAuthToken: '<My Azure OAuth Token>'}

and then I try to pass up the secret to the action via

  ...
          msAuthToken:
            title: Microsoft Auth Token
            ui:field: MicrosotOauthTokenProvider
            ui:backstage:
              review:
                show: false # won't print any info about 'hidden' property on Review Step
...
  steps:
    - id: scm-create-project
      name: Create the project in SCM
      action: scm-create-project
      input:
        ...
        accessToken: ${{ secrets.msAuthToken }}

but when I submit the form I get a bad request InputError: Invalid input passed to action scm-create-project, instance requires property "accessToken"

If I replace {{ secrets.msAuthToken }} with a static string like 'foo' the info is correctly passed along and I don't get that invalid input error

vital stump
#

and I can see the in the payload to POST to api/scaffolder/v2/dry-run template.secrets is indeed empty

vital stump
#

@fiery marsh

with this code

export const MicrosotOauthTokenProvider = ({
  name,

}: FieldProps<string>) => {
  const microsoftAuthApi= useApi(microsoftAuthApiRef);
  const { setSecrets, secrets } = useTemplateSecrets();
  useDebounce( async() => {
    const accessToken = await microsoftAuthApi.getAccessToken();
    setSecrets({
      [name]: accessToken
    });
    console.log(secrets);
  });
  return (<FormControl/>);
};

the token I retrieve has an audience
"aud": "00000003-0000-0000-c000-000000000000",
00000003-0000-0000-c000-000000000000 is Microsoft Graph (https://learn.microsoft.com/en-us/troubleshoot/azure/active-directory/verify-first-party-apps-sign-in)
I would have expected "aud": "<backstage's client ID>"
am I using the wrong apiRef to fetch the access token ?

Describes how to verify first-party Microsoft applications in sign-in reports.

vital stump
#

i see that I get a token with the appropriate aud if I specify
const accessToken = await microsoftAuthApi.getAccessToken('api://<backstage_client_id>/user_impersonation');

#

however im trying to retrieve <backstage_client_id> via configApiRef

  const config = useApi(configApiRef);
  const authConfig = config.getConfig('auth');
  const authEnv = authConfig.getString('environment');
  const msAuthProviderConfig = authConfig.getConfig('providers').getConfig('microsoft').getConfig(authEnv)

but authConfig.getConfig('providers').getConfig('microsoft') is empty
the same code on the backend returns the expected config

vital stump
#

oh I see

The visibility only applies to the direct parent of where the keyword is placed in the schema. For example, if you set the visibility to frontend for a subset of the schema with type: "object", but none of the descendants, only an empty object will be available in the frontend. The full ancestry does not need to have correctly defined visibilities however, so it is enough to only for example declare the visibility of a leaf node of type: "string".

vital stump
#

so how should I go about to retrieve my backstage clientId at the frontend level ?

drifting umbra
#

You can never under any circumstance access config in the frontend that's not explicitly marked with frontend visibility

#

It's just not possible but design for security reasons

#

And indeed the visibility does not propagate to children

#

The microsoftAuthApiRef is different; it let's you negotiate an oauth access token that can be used in requests to do things on your behalf

#

That is done with the assistance of the auth backend

vital stump
#

yes but when I am calling await microsoftAuthApi.getAccessToken() without specifying a scope I am getting a token with the "aud": "00000003-0000-0000-c000-000000000000" which is Microsoft Graph

#

I would like to get a token with "aud": "<backstage client id>"

vital stump
#

from the same microsoftAuthApi if do getIdToken() I get an id token with "aud": "<backstage client id>"

indigo ice
#

This sounds like it's somewhat related to https://github.com/backstage/backstage/issues/22644 - when you request scopes, you'll need to fully qualify the scopes (e.g. <backstage client id>/.default), or we'll need to update the microsoftAuthApi to allow you to explicitly specify the resource to use.

GitHub

📜 Description The repo picker doesn't work out of the box with Azure devops due to the scopes in the default ScmAuthApi implementation not fully qualifying the scope names. As the scopes aren&#...

vital stump
#

but here im stuck then because I need the <backstage client id> as scope but I cannot retrieve it at the from the config at the frontend because of the @visibility of that config element

indigo ice
drifting umbra
#

Got to be explicitly marked frontend unfortunately