In my Sparatacus 2.0 application, my APIs are getting invalidated after the access_token times out and gives this error.
{
"error":"invalid_token",
"error_description":"Invalid access token: 14399ab4-2e63-4ee2-a0a8-3ff493e09142"
}
The authentication response after login is this
{
"access_token" : "14399ab4-2e63-4ee2-a0a8-3ff493e09142",
"token_type" : "bearer",
"refresh_token" : "5c28d5b1-c978-4e38-adfa-c1e41ef97b9c",
"expires_in" : 13,
"scope" : "basic openid"
}
Do I need to explicitly set the refresh token in the frontend by calling some OOTB Spartacus method?
I had a similar issue in Spartacus 3.0 . The refresh token was only stored (in the memory) as long I was in the same browser tab after the login, when I refreshed the page the user was only authenticated until the access token expired.
The issue for me was that the AuthStatePersistenceService was not saving the refresh token in the local storage and because of that it was not anymore there after a page reload. I was able to fix it with overwriting the getAuthState function in this service like that:
import { combineLatest, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { Injectable } from '#angular/core';
import { AuthStatePersistenceService, SyncedAuthState } from '#spartacus/core';
#Injectable()
export class MyAuthStatePersistenceService extends AuthStatePersistenceService {
/**
* Gets and transforms state from different sources into the form that should
* be saved in storage.
*/
protected getAuthState(): Observable<SyncedAuthState> {
return combineLatest([
this.authStorageService.getToken().pipe(
filter((state) => !!state),
map((state) => {
return {
...state,
};
}),
),
this.userIdService.getUserId(),
this.authRedirectStorageService.getRedirectUrl(),
]).pipe(
map(([authToken, userId, redirectUrl]) => {
let token = authToken;
if (token) {
token = { ...token };
}
return { token, userId, redirectUrl };
}),
);
}
}
Maybe that helps you.
There was an existing issue with spartacus and it’s been fixed.
If you want to fix this issue just update https://github.com/SAP/spartacus/pull/4443/commits/0cbf839ca4b6dda195730a81fcce5881bc70be39#diff-f13978c59296903fbcc824e50a0563fbf3dc1583b818c3769d0e1f84fc99d036
According to this commit.
Related
I need to retrieve a customer connected to the storefront backend side to reward him in different ways.
I created a plugin that extends the plugin.class of the plugins system.
It fetches the customer on the store api using the route store-api/account/customer then it sends to my backend its identifier. I also resolve the shop_url of the admin api with window.location.protocol and window.location.hostname...
This seems to me not secured or accurate (the domain can be different from the sales channel to the admin api) and I would like to know if it would be possible to fetch a secured unique customer's token that would allow me to resolve both the shop_url and the customer's identifier.
I cannot find anything in the documentation that would help me securing that part of my app.
Thanks.
(Edit)
Here is my actual code to fetch the customer inside the plugin:
import Plugin from 'src/plugin-system/plugin.class';
import StoreApiClient from 'src/service/store-api-client.service';
const storeClient = new StoreApiClient();
const handleUser = (data, request) => {
let unsecuredUserId = null;
if (request.status === 200) {
try {
const user = JSON.parse(data);
unsecuredUserId = user.id || null;
} catch (e) {}
}
doSomethingWith(unsecuredUserId);
}
export default class SaylPlugin extends Plugin {
init() {
storeClient.get('store-api/account/customer', handleUser);
}
}
You have access to the customer object in the twig template, given the user is currently logged-in. Using this fact you can pass customer data to your plugin using data attributes. The plugin base offers automatic parsing of options based on the naming convention.
{% set myPluginData = {
customerId: context.customer.id
} %}
<div data-my-custom-plugin="true"
data-my-custom-plugin-options="{{ myPluginData|json_encode }}">
</div>
class MyCustomPlugin extends Plugin {
init() {
if (this.options.customerId) {
// do something when the customer is logged in
}
}
// ...
}
PluginManager.register('MyCustomPlugin', MyCustomPlugin, '[data-my-custom-plugin]');
I finally found a way to get the things more secured.
My new plugin code:
import Plugin from 'src/plugin-system/plugin.class';
import StoreApiClient from 'src/service/store-api-client.service';
const storeClient = new StoreApiClient();
const handleContext = (data, request) => {
if (request.status === 200) {
try {
const context = JSON.parse(data);
if (context instanceof Object) {
resolveCustomerBackendSide(
context.token,
context.salesChannel.id
);
}
} catch (e) {
console.error(e);
}
}
}
export default class SaylPlugin extends Plugin {
init() {
storeClient.get('store-api/context', handleContext);
}
}
With this context I can resolve the admin api credentials backend side using the sales channel identifier that I save during the app registration process (you will have to allow sales_channel read in the app's manifest). Therefore I fetch the sales channel backend side to retrieve the sw-access-key header and I can finally fetch the store-api backend side to retrieve the customer in a secured way (the token that you get after fetching the store-api/context can be used as sw-context-token header.
I have many services that all need to know the tenant ID from the request (kept in JWT auth token). The request is either GRPC (jwt stored in MetaData) or Graphql (jwt stored in context.headers.authorization).
I would like to be able to force myself not to forget to pass this tenant id when using the services. Ideally I dont want to even have to constantly write the same code to get the info from the request and pass it through. However the only ways I've managed to do it was using:
#Inject(REQUEST) for grpc in the service constructor. This doesn't work for the graphql requests. The only other way I saw was to only return service methods AFTER providing the data, which looks ugly as hell:
class MyService {
private _actions: {
myMethod1() { ... }
}
withTenantDetails(details) {
this._details = details;
return this._actions;
}
}
If I can somehow get the execution context within MyService that would be a good option, and make this easy using:
const getTenantId = (context: ExecutionContext) => {
if (context.getType() === 'rpc') {
logger.debug('received rpc request');
const request = context.switchToRpc().getContext();
const token = request.context.get("x-authorization");
return {
token,
id: parseTokenTenantInfo(token)
};
}
else if (context.getType<GqlContextType>() === 'graphql') {
logger.debug('received graphql request');
const gqlContext = GqlExecutionContext.create(context);
const request = gqlContext.getContext().request;
const token = request.get('Authorization');
return {
token,
id: parseTokenTenantInfo(token)
};
}
else {
throw new Error(`Unknown context type receiving in tenant param decorator`)
}
}
But I can't find any way to get the executioncontext across to the service without also having to remember to pass it every time.
It's possible to inject Request into injectable service.
For that, the Service will be Scope.Request, and no more Singleton, so a new instance will be created for each request. It's an important consideration, to avoid creating too many resources for performance reason.
It's possible to explicit this scope with :
#Injectable({ scope: Scope.REQUEST })
app.service.ts :
#Injectable({ scope: Scope.REQUEST })
export class AppService {
tenantId: string;
constructor(#Inject(REQUEST) private request: Request) {
// because of #Inject(REQUEST),
// this service becomes REQUEST SCOPED
// and no more SINGLETON
// so this will be executed for each request
this.tenantId = getTenantIdFromRequest(this.request);
}
getData(): Data {
// some logic here
return {
tenantId: this.tenantId,
//...
};
}
}
// this is for example...
const getTenantIdFromRequest = (request: Request): string => {
return request?.header('tenant_id');
};
Note that, instead of decode a JWT token in order to retrieve TENANT_ID for each request, and maybe for other service (one per service), an other approach could be to decode JWT one single time, and then add it in Request object.
It could be done with a global Guard, same as authorization guard examples of official docs.
Here just a simple example : (could be merged with a Auth Guard)
#Injectable()
export class TenantIdGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
request['tenantId'] = getTenantIdFromRequest(request);
return true; // or any other validation
}
}
For GraphQL applications, we should inject CONTEXT in place of REQUEST :
constructor(#Inject(CONTEXT) private context) {}
You have to set either request inside context, or directly TENANT_ID inside context in order to retrieve it after inside service.
After logging in using the MsalAuthenticationTemplate InteractionType.Redirect, how do I get the JWT returned after successful authentication? It does not seem to be included in the msal instance.
import { MsalProvider, MsalAuthenticationTemplate, useMsal, useAccount } from "#azure/msal-react";
const { instance } = useMsal();
You should call acquireTokenSilent each time you need an access token. You can read more in our getting started doc and also review the msal-react-samples
Another way of getting the idToken(JWT) after successful login is to hook into the addEventCallback and check for EventType.LOGIN_SUCCESS.
const callbackId = instance.addEventCallback(message => {
if (message.eventType === EventType.LOGIN_SUCCESS) {
const { payload } = message;
// Get idToken(JWT) from the payload.
console.log(payload.idToken);
}
})
https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/events.md
I have created a simple jwt auth application the same way its displayed here: https://github.com/raymondfeng/loopback4-example-auth0
The authentication part is working properly but the authorization does not work as expected.
I decorated my controller with following function and added a scope.
#authenticate({strategy: 'auth0-jwt', options: {scopes: ['greet']}})
In my authentication strategy I´m checking the scope via the AuthenticationMetadata class.
import {AuthenticationBindings, AuthenticationMetadata, AuthenticationStrategy} from '#loopback/authentication';
import {inject} from '#loopback/core';
import {ExpressRequestHandler, Request, Response, RestBindings} from '#loopback/rest';
import {UserProfile} from '#loopback/security';
import {JWT_SERVICE} from './types';
const jwtAuthz = require('express-jwt-authz');
export class JWTAuthenticationStrategy implements AuthenticationStrategy {
name = 'auth0-jwt';
constructor(
#inject(RestBindings.Http.RESPONSE)
private response: Response,
#inject(AuthenticationBindings.METADATA)
private metadata: AuthenticationMetadata,
#inject(JWT_SERVICE)
private jwtCheck: ExpressRequestHandler,
) {}
async authenticate(request: Request): Promise<UserProfile | undefined> {
return new Promise<UserProfile | undefined>((resolve, reject) => {
this.jwtCheck(request, this.response, (err: unknown) => {
if (err) {
console.error(err);
reject(err);
return;
}
console.log(this.metadata.options);
// If the `#authenticate` requires `scopes` check
if (this.metadata.options?.scopes) {
jwtAuthz(this.metadata.options!.scopes, {failWithError: true})(request, this.response, (err2?: Error) => {
if (err2) {
console.error(err2);
reject(err2);
return;
}
// eslint-disable-next-line #typescript-eslint/no-explicit-any
resolve((request as any).user);
});
} else {
// eslint-disable-next-line #typescript-eslint/no-explicit-any
resolve((request as any).user);
}
});
});
}
}
When trying to access
this.metadata.options
I´m always getting an undefined back.
How can I achieve to get the options and the scope out of metadata?
Thanks
For Loopback Authorization your class needs to implement the Provider<Authorizer> interface. In that interface it defines the 2 functions you need to implement
#injectable({scope: BindingScope.TRANSIENT})
class AuthorizationService implements Provider<Authorizer>{
value (): Authorizer {
return this.authorize.bind(this);
}
async authorize (
context: AuthorizationContext,
metadata: AuthorizationMetadata,
) {
// TODO implement authorization
}
}
The authorization metadata will be injected by loopback into that function automatically after you bind it with an AuthorizationTags.Authorizer
If you are having problems implementing Authentication then read my step by step guide on how we implemented Loopback Authentication using Firebase. That should be able to help you with the core ideas to get Authentication running.
I'm struggling to solve an issue that I got on trying to login via Single Sign On from Microsoft Azure using Cypress. It is possible to do it without using the Client_Secret? How can I do it?
I've been spending more than a week trying to solve this situation...
I'm a junior on this, so if you could help-me I would be very grateful.
Thanks a lot,
Yes, you can. Navigate to your AD App in the portal -> Authentication -> set Allow public client flows to Yes like below.
Then in the blog, in step Cypress utility for mimicking react-adal, it uses the client credential flow, there is a comment post by #Bryce Kolton under the blog, he uses the ROPC flow, in this flow, you could use it without Client_Secret via a public client App as you changed above(Allow public client flows), just refer to it.
/* eslint-disable no-underscore-dangle */
import { AuthenticationContext } from ‘react-adal’;
import { azTenantId, azClientId } from ‘../../server/config/environment’;
// Need to get data points from server’s environment, not src
const adalConfig = {
tenant: azTenantId,
clientId: azClientId,
cacheLocation: ‘localStorage’,
replyUrl: ‘/’,
endpoints: {
api: ”,
},
};
const authContext = new AuthenticationContext(adalConfig);
export default async function doLogin() {
// getCachedToken also does an expiration check so we know for sure the tokens are usable
if (
!authContext.getCachedToken(adalConfig.endpoints.api)
|| !authContext.getCachedToken(adalConfig.clientId)
) {
const response = await cy.request({
method: ‘POST’,
url:
‘https://login.microsoftonline.com/mercedesme.onmicrosoft.com/oauth2/token’,
// qs: { ‘api-version’: ‘1.0’ }, // uncomment if your consuming resource expects the ‘aud’ to have a prefix of ‘sn:’
headers: {
‘cache-control’: ‘no-cache’,
‘content-type’:
‘multipart/form-data; boundary=—-WebKitFormBoundary7MA4YWxkTrZu0gW’,
},
form: true,
body: {
grant_type: ‘password’,
response_type: ‘code’,
client_id: ‘[[yourappsclientid]]’,
username: ‘[[yourtestuzseremail]]’,
password: ‘[[yourtestuserpassword]]!’,
scope: ‘openid’,
resource: ‘[[some-resource-id]]’,
},
});
// Store the token and data in the location where adal expects it
authContext._saveItem(authContext.CONSTANTS.STORAGE.IDTOKEN, response.body.access_token);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + adalConfig.endpoints.api,
response.body.access_token,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + adalConfig.clientId,
response.body.access_token,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.EXPIRATION_KEY + adalConfig.endpoints.api,
response.body.expires_on,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.EXPIRATION_KEY + adalConfig.clientId,
response.body.expires_on,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.TOKEN_KEYS,
[adalConfig.clientId].join(authContext.CONSTANTS.RESOURCE_DELIMETER)
+ authContext.CONSTANTS.RESOURCE_DELIMETER,
);
}
}
To use the ROPC flow successfully, make sure your scenario meets the requirements below, e.g. your user account is not MAF-enabled.