Skip to main content

Adding webview customizations using external authentication

When adding a webview to extend the capabilities of the Associate App, external services or APIs used in this webview may require authentication. For this scenario, two different integrations are necessary.

Important

Do not hard-code credentials in the code of a webview!

Retrieving external authentication information

The Associate App offers a mechanism to retrieve an external authentication token after the user authenticated in the app. For that, use the Associate App configuration api to set the configuration for post_login_callback_urls.

For an example, see how you can use this property to set up the callback URL for loyalty rewards integration and the associated identity provider here .

{
"external_integration": {
"post_login_callback_urls": [
{
"identity_provider": "external-integration",
"url": "https://integration.site/auth/callback"
},
{
"identity_provider": "customer-loyalty",
"url": "https://customer.loyalty.callback.url.com"
}
]
}
}

The API configured here will be invoked after every successful login in Associate App with a payload containing a NewStore identity auth token and device identifier.

{
"device_id": "",
"identity": "Bearer {NEWSTORE-JWT-TOKEN}"
}

In this API, first verify the NewStore token, then create a valid token for your third-party service. The endpoint is expected to return authentication identifier which will be handled as string and stored in the local storage of the app. NewStore does not care about the format, it can even be serialized data. An example implementation can be found here .

{
"external_identity": "{TOKEN-VALID-FOR-EXTERNAL-PROVIDER}"
}

Using data in a webview component

After setting up a webview customization (Managing Associate App customizations ) for one of the offered locations, the given url will be loaded with (context-aware) data attached as hash. The data can be accessed using Javascript and we recommend having a fallback for local development:

<script type="application/javascript">
if (!window.NEWSTORE) {
if (new URL(window.location.href).hash) {
window.NEWSTORE = JSON.parse(
atob(new URL(window.location.href).hash.replace(/^#/, ""))
);
} else {
// fallback for local development
window.NEWSTORE = {
contextProps: {
formData: {
email_optin: "true",
},
},
};
}
}
</script>

Every data schema passed into a webview contain three default properties:

  • {associateId}: The ID of the associate currently using Associate App,
  • {storeId}: The ID of the store where the associate is currently operating in,
  • {cartId}: The ID of the current cart, if one was created.

The remaining schema of the data depends on the location. For customer profile extended attributes, the schema contains all extended attributes of the customer profile in contextProps.formData as key-value pairs.

{
"contextProps": {
"formData": {
"emailOptin": "true",
"favoriteColor": "Blue",
"associateId": "1db2a42d-e872-432a-a87f-ed0964efc8d2",
"storeId": "d4acdd7f-b6ef-4c4a-b229-250d60e745fb",
"cartId": "d816a750-bcdf-4858-bca4-adca283be8b3",
}
},
"externalIdentity": "secret-token-123",
"externalIdentities": [{
"identityProvider": "external-integration",
"identity": "secret-token-for-external-integration",
},
{
"identityProvider": "customer-loyalty",
"identity": "secret-token-for-customer-loyalty",
}],
}

The More menu webview customization contains the following context properties.

{
contextProps: {
formData: {
storeInfo: {
label: string
physicalAddress: {
addressLine1: string
addressLine2?: string
province?: string
state?: string
zipCode?: string
city?: string
countryCode: string
latitude?: number
longitude?: number
}
shippingAddress?: {
addressLine1: string
addressLine2?: string
province?: string
state?: string
zipCode?: string
city?: string
countryCode: string
latitude?: number
longitude?: number
}
divisionName?: string
managerId?: string
imageUrl?: string
phoneNumber?: string
activeStatus: boolean,
supportedShippingMethods: Array<
| 'traditional_carrier'
| 'same_day_delivery'
| 'in_store_pick_up'
| 'in_store_handover'
>
giftWrapping: boolean
pricebook?: string
deliveryZipCodes: string[]
shippingProviderInfo: {
[key: string]: any
}
businessHours: Array<{
fromTime: string
toTime: string
weekday: number
earliestPickUp?: string
latestPickUp?: string
}>
timezone: string
taxId?: string
taxIncluded: boolean
queuePrioritization: Array<{
priority: number
shippingType:
| 'traditional_carrier'
| 'same_day_delivery'
| 'in_store_pick_up'
| 'DHL_PAKET'
displayPriorityType?: 'priority' | 'normal'
}>
catalog?: string
locale?: string
storeId: string
displayPriceUnitType: 'net' | 'gross'
}
userInfo: {
id: string
email: string
firstName: string
lastName: string
telephoneNumber: string
storeId: string
imageUrl: string
createdAt: Date
updatedAt: Date
isActive: boolean
printerLocationId: string | null
printerLocation?: {
uuid: string
name: string
}
}
associateId: string
storeId: string
cartId: string
}
}
}

Webview customization examples

Webview: VanillaJS
<html lang="en">

<head>
<title>Test App</title>
<meta name="viewport" content="user-scalable=no, width=device-width">
<script type="application/javascript">
if (!window.NEWSTORE) {
if (new URL(window.location.href).hash) {
window.NEWSTORE = JSON.parse(atob(new URL(window.location.href).hash.replace(/^#/, "")));
} else {
// fallback for local development
window.NEWSTORE = {
contextProps: {
formData: {
email_optin: 'true'
}
}
}
}
}
</script>
<style>
label,
input {
display: block;
}
</style>
</head>

<body>
<div id="extended-attributes"></div>
<script type="application/javascript">
if (window.NEWSTORE && window.NEWSTORE.contextProps) {
const translation = {
email_optin: 'Wants to receive emails from us'
}

Object.entries(window.NEWSTORE.contextProps.formData).map(function (entry) {
const key = entry[0];
const value = entry[1];
const label = document.createElement("label");
const input = document.createElement("input");
label.innerText = translation[key] || key
input.type = 'text'
input.name = key
input.value = value
label.append(input)
document.getElementById("extended-attributes").append(label);
});
}
</script>
</body>

</html>
Webview: ReactJS
<html lang="en">

<head>
<title>React Example</title>
<meta name="viewport" content="user-scalable=no, width=device-width">
<script type="application/javascript">
if (!window.NEWSTORE) {
if (new URL(window.location.href).hash) {
window.NEWSTORE = JSON.parse(atob(new URL(window.location.href).hash.replace(/^#/, "")));
} else {
// fallback for local development
window.NEWSTORE = {
contextProps: {
formData: {
loyalty_id: '1234567890'
}
}
}
}
}
</script>

<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

<style>
label,
input {
display: block;
}
</style>
</head>

<body>
<div id="extended-attributes"></div>

<script type="text/babel">
if (window.NEWSTORE && window.NEWSTORE.contextProps) {
const domContainer = document.querySelector('#extended-attributes');
const root = ReactDOM.createRoot(domContainer);
const translation = {
loyalty_id: 'Loyalty ID'
}

const LabelledInput = function (props) {
return (
<label>
{props.name}
<input type="text" name={props.name} value={props.value} onChange={() => null} />
</label>
)
}

const Form = () => {
return Object.entries(window.NEWSTORE.contextProps.formData).map(([name, value]) => (
<LabelledInput key={name} name={translation[name] || name} value={value} />
))
}

root.render(<Form />);
}
</script>
</body>

</html>
Webview: Using external APIs
<html lang="en">

<head>
<title>React Example</title>
<meta name="viewport" content="user-scalable=no, width=device-width">
<script type="application/javascript">
if (!window.NEWSTORE) {
if (new URL(window.location.href).hash) {
window.NEWSTORE = JSON.parse(atob(new URL(window.location.href).hash.replace(/^#/, "")));
} else {
// fallback for local development
window.NEWSTORE = {
contextProps: {
formData: {
'External Customer ID': '2'
}
}
}
}
}
</script>

<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/@tanstack/react-query@4/build/umd/index.production.js"></script>
<script src="https://unpkg.com/axios@0.27.2/dist/axios.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

<style>
label,
input {
display: block;
font-family: Tahoma, serif;
width: 100%;
}

label {
color: #aaaaaa;
margin-bottom: 1rem;
font-size: 0.8rem;
}

input {
font-size: 1rem;
border: 1px solid #aaa;
border-radius: 6px;
padding: 8px;
}
</style>
</head>

<body>
<div id="extended-attributes"></div>

<script type="text/babel">
if (window.NEWSTORE && window.NEWSTORE.contextProps) {
const domContainer = document.querySelector('#extended-attributes');
const root = ReactDOM.createRoot(domContainer);
const queryClient = new window.ReactQuery.QueryClient();
const QueryClientProvider = window.ReactQuery.QueryClientProvider;
const useQuery = window.ReactQuery.useQuery;

const external_user_id = window.NEWSTORE.contextProps.formData["External Customer ID"];

const LabelledInput = function (props) {
return (
<label>
{props.name}
<input type="text" name={props.name} value={props.value} onChange={() => null} />
</label>
)
}

const Form = () => {
const { isLoading, isError, data, error } = useQuery(['customer'], () =>
// axios.get("https://jsonplaceholder.typicode.com/users/"+external_user_id)
axios.get("https://jsonplaceholder.typicode.com/users/2")
.then((res) => res.data)
)

if (isLoading) {
return <span>Loading...</span>
}

if (isError) {
return <span>Error: {error.message}</span>
}

return (
<div>
<LabelledInput name="External Customer ID" value={external_user_id} />
<LabelledInput name="Username" value={data.username} />
<LabelledInput name="Name" value={data.name} />
<LabelledInput name="Email" value={data.email} />
<LabelledInput name="Phone" value={data.phone} />
</div>
)
}

root.render(
<QueryClientProvider client={queryClient}>
<Form />
</QueryClientProvider>
);
}
</script>
</body>

</html>
Sample implementation to exchange authentication token (python)
"""
Sample python NewStore identity forwarding service implementation.
"""
from flask import Flask, jsonify, request
from flask_cors import CORS
from uuid import uuid4
from jwt import PyJWKClient

import requests
import jwt
import os

OPENID_CONFIG_URL = "https://id.p.newstore.net/auth/realms/{0}/.well-known/openid-configuration"
TENANT = os.getenv('NEWSTORE_TENANT')

app = Flask(__name__)
CORS(app=app)

class APIError(Exception):
def __init__(self, *args: object) -> None:
super().__init__(*args)

if "x-request-id" in request.headers:
self.request_id = request.headers["x-request-id"]
else:
self.request_id = uuid4()

class BadRequestError(APIError):
status_code = 400

def __init__(self, message: str) -> None:
super().__init__(self)
self.message = message

class UnauthorizedError(APIError):
status_code = 401

def __init__(self, message: str) -> None:
super().__init__(self)
self.message = message

class InternalError(APIError):
status_code = 401

def __init__(self, message: str) -> None:
super().__init__(self)
self.message = message

def get_jwks_url() ->str:
response = requests.get(OPENID_CONFIG_URL.format(TENANT))

if not response.ok:
raise InternalError("could not load JWKS")

data = response.json()
return data['jwks_uri']

def get_external_identity(claims: dict) -> str:
"""
Code to retrieve the token.
All 3rd party specific logic is implemented here from retrieving the required claims to issuing requests to 3rd party systems.
"""

# Extract any claims that are needed to establish the identity of the person, such as preferred_username, email, ...
preferred_username = claims['preferred_username']

# In this example we are just returning the preferred_username claim.
return preferred_username

@app.errorhandler(APIError)
def handle_invalid_usage(error):
"""Handles thrown exceptions."""
response = jsonify({
"request_id": error.request_id,
"code": error.status_code,
"message": error.message
})
response.status_code = error.status_code
return response


@app.route("/exchanges", methods=["POST"])
def exchange_identity() -> dict:
"""Performs identity exchange based on the JWT token data."""

# Extract token from request
newstore_identity = request.json['identity']
if not newstore_identity:
raise BadRequestError('invalid request body')

# Decode and verify token
try:
jwks_client = PyJWKClient(get_jwks_url())
signing_key = jwks_client.get_signing_key_from_jwt(newstore_identity)

# Verify token and standard claims according to https://pyjwt.readthedocs.io/en/stable/api.html
# plus make sure preferred_username and 'http://newstore/tenant' claims exist
data = jwt.decode(
newstore_identity,
signing_key.key,
algorithms = ["RS256"],
audience = TENANT,
options = {
"verify_signature": True,
"require": ["preferred_username", "http://newstore/tenant"]
},
)
except Exception as e:
raise UnauthorizedError("invalid token")

if data['http://newstore/tenant'] != TENANT:
raise UnauthorizedError("invalid token tenant")

# Retrieve the 3rd party token based on the claims in the JWT.
external_identity = get_external_identity(data)
return {"external_identity": external_identity}


if __name__ == "__main__":
app.run(port=8080, debug=True)

Related topics