Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions aws/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,21 @@ Parameters:
proxyRoot:
Type: String
Description: proxy root
authorityUri:
cognitoHost:
Type: String
Description: OIDC authority uri
Description: Cognito host
cognitoClientId:
Type: String
Description: Cognito client id
cognitoDomainPrefix:
Type: String
Description: Cognito domain prefix
entraLogoutUri:
Type: String
Description: Entra logout uri
legacyAuthorityUri:
Type: String
Description: Legacy OIDC authority uri
databaseHost:
Type: String
Description: Database host
Expand Down Expand Up @@ -163,8 +175,16 @@ Resources:
Value: !Ref appRoot
- Name: PROXY_ROOT
Value: !Ref proxyRoot
- Name: AUTHORITY_URI
Value: !Ref authorityUri
- Name: LEGACY_AUTHORITY_URI
Value: !Ref legacyAuthorityUri
- Name: COGNITO_HOST
Value: !Ref cognitoHost
- Name: COGNITO_CLIENT_ID
Value: !Ref cognitoClientId
- Name: COGNITO_DOMAIN_PREFIX
Value: !Ref cognitoDomainPrefix
- Name: ENTRA_LOGOUT_URI
Value: !Ref entraLogoutUri
- Name: SMTP_HOSTNAME
Value: !Ref smtpHostname
- Name: PDF_SERVICE_ROOT
Expand Down
4 changes: 3 additions & 1 deletion scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ if [ "${TRAVIS_BRANCH}" = "main" ]; then
aws s3 cp s3://$S3_BUCKET_NAME/purchasing/production.env ./secrets.env

STACK_NAME=purchasing
LEGACY_AUTHORITY_URI=https://www.linn.co.uk/auth/
APP_ROOT=http://app.linn.co.uk
PROXY_ROOT=http://app.linn.co.uk
ENV_SUFFIX=
Expand All @@ -25,6 +26,7 @@ if [ "${TRAVIS_BRANCH}" = "main" ]; then
aws s3 cp s3://$S3_BUCKET_NAME/purchasing/sys.env ./secrets.env

STACK_NAME=purchasing-sys
LEGACY_AUTHORITY_URI=https://www-sys.linn.co.uk/auth/
APP_ROOT=http://app-sys.linn.co.uk
PROXY_ROOT=http://app.linn.co.uk
ENV_SUFFIX=-sys
Expand All @@ -38,6 +40,6 @@ fi
source ./secrets.env > /dev/null 2>&1

# deploy the service to amazon
aws cloudformation deploy --stack-name $STACK_NAME --template-file ./aws/application.yml --parameter-overrides dockerTag=$TRAVIS_BUILD_NUMBER databaseHost=$DATABASE_HOST databaseName=$DATABASE_NAME databaseUserId=$DATABASE_USER_ID databasePassword=$DATABASE_PASSWORD rabbitServer=$RABBIT_SERVER rabbitPort=$RABBIT_PORT rabbitUsername=$RABBIT_USERNAME rabbitPassword=$RABBIT_PASSWORD appRoot=$APP_ROOT proxyRoot=$PROXY_ROOT authorityUri=$AUTHORITY_URI loggingEnvironment=$LOG_ENVIRONMENT loggingMaxInnerExceptionDepth=$LOG_MAX_INNER_EXCEPTION_DEPTH smtpHostname=$SMTP_HOSTNAME pdfServiceRoot=$PDF_SERVICE_ROOT htmlToPdfApiConversionEndpoint=$HTML_TO_PDF_API_CONVERSION_ENDPOINT viewsRoot=$VIEWS_ROOT purchasingFromAddress=$PURCHASING_FROM_ADDRESS logisticsToAddress=$LOGISTICS_TO_ADDRESS orderBookTestAddress=$ORDER_BOOK_TEST_ADDRESS acknowledgementsBcc=$ACKNOWLEDGEMENTS_BCC environmentSuffix=$ENV_SUFFIX --capabilities=CAPABILITY_IAM
aws cloudformation deploy --stack-name $STACK_NAME --template-file ./aws/application.yml --parameter-overrides dockerTag=$TRAVIS_BUILD_NUMBER databaseHost=$DATABASE_HOST databaseName=$DATABASE_NAME databaseUserId=$DATABASE_USER_ID databasePassword=$DATABASE_PASSWORD rabbitServer=$RABBIT_SERVER rabbitPort=$RABBIT_PORT rabbitUsername=$RABBIT_USERNAME rabbitPassword=$RABBIT_PASSWORD appRoot=$APP_ROOT proxyRoot=$PROXY_ROOT legacyAuthorityUri=$LEGACY_AUTHORITY_URI cognitoHost=$COGNITO_HOST cognitoClientId=$COGNITO_CLIENT_ID cognitoDomainPrefix=$COGNITO_DOMAIN_PREFIX entraLogoutUri=$ENTRA_LOGOUT_URI authorityUri=$AUTHORITY_URI loggingEnvironment=$LOG_ENVIRONMENT loggingMaxInnerExceptionDepth=$LOG_MAX_INNER_EXCEPTION_DEPTH smtpHostname=$SMTP_HOSTNAME pdfServiceRoot=$PDF_SERVICE_ROOT htmlToPdfApiConversionEndpoint=$HTML_TO_PDF_API_CONVERSION_ENDPOINT viewsRoot=$VIEWS_ROOT purchasingFromAddress=$PURCHASING_FROM_ADDRESS logisticsToAddress=$LOGISTICS_TO_ADDRESS orderBookTestAddress=$ORDER_BOOK_TEST_ADDRESS acknowledgementsBcc=$ACKNOWLEDGEMENTS_BCC environmentSuffix=$ENV_SUFFIX --capabilities=CAPABILITY_IAM

echo "deploy complete"
62 changes: 62 additions & 0 deletions src/Service.Host/MultiAuthHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace Linn.Purchasing.Service.Host
{
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

public class MultiAuthHandler : AuthenticationHandler<MultiAuthOptions>
{
private readonly JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler();

public MultiAuthHandler(
IOptionsMonitor<MultiAuthOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder)
{
}

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var cognitoIssuer = this.Options.CognitoIssuer;
var cognitoScheme = this.Options.CognitoScheme;
var legacyScheme = this.Options.LegacyScheme;

if (!this.Request.Headers.TryGetValue("Authorization", out var hdr))
{
return await this.Context.AuthenticateAsync(legacyScheme);
}

var header = hdr.ToString();
if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
return await this.Context.AuthenticateAsync(legacyScheme);
}

var token = header.Substring("Bearer ".Length).Trim();

JwtSecurityToken jwt;
try
{
jwt = this.jwtHandler.ReadJwtToken(token);
}
catch
{
return await this.Context.AuthenticateAsync(legacyScheme);
}

// decide which scheme to forward to based on the issuer
if (jwt.Issuer != null && jwt.Issuer.Equals(cognitoIssuer, StringComparison.OrdinalIgnoreCase))
{
return await this.Context.AuthenticateAsync(cognitoScheme);
}

return await this.Context.AuthenticateAsync(legacyScheme);
}
}
}
13 changes: 13 additions & 0 deletions src/Service.Host/MultiAuthOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Linn.Purchasing.Service.Host
{
using Microsoft.AspNetCore.Authentication;

public class MultiAuthOptions : AuthenticationSchemeOptions
{
public string CognitoIssuer { get; set; }

public string CognitoScheme { get; set; }

public string LegacyScheme { get; set; }
}
}
9 changes: 6 additions & 3 deletions src/Service.Host/Negotiators/HtmlNegotiator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,14 @@ public async Task Handle(HttpRequest req, HttpResponse res, object model, Cancel
var template = Handlebars.Compile(view);

var jsonAppSettings = JsonConvert.SerializeObject(
new
new ApplicationSettings
{
AuthorityUri = ConfigurationManager.Configuration["AUTHORITY_URI"],
AppRoot = ConfigurationManager.Configuration["APP_ROOT"],
ProxyRoot = ConfigurationManager.Configuration["PROXY_ROOT"]
ProxyRoot = ConfigurationManager.Configuration["PROXY_ROOT"],
CognitoHost = ConfigurationManager.Configuration["COGNITO_HOST"],
CognitoClientId = ConfigurationManager.Configuration["COGNITO_CLIENT_ID"],
CognitoDomainPrefix = ConfigurationManager.Configuration["COGNITO_DOMAIN_PREFIX"],
EntraLogoutUri = ConfigurationManager.Configuration["ENTRA_LOGOUT_URI"]
},
Formatting.Indented,
new JsonSerializerSettings
Expand Down
43 changes: 42 additions & 1 deletion src/Service.Host/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using Linn.Purchasing.Service.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;

namespace Linn.Purchasing.Service.Host
{
using System.IdentityModel.Tokens.Jwt;
Expand Down Expand Up @@ -50,13 +53,51 @@ public void ConfigureServices(IServiceCollection services)
services.AddMessageDispatchers();

services.AddCarter();

var appSettings = ApplicationSettings.Get();

const string LegacyJwtScheme = JwtBearerDefaults.AuthenticationScheme;

services.AddLinnAuthentication(
options =>
{
options.Authority = ConfigurationManager.Configuration["AUTHORITY_URI"];
options.Authority = ConfigurationManager.Configuration["LEGACY_AUTHORITY_URI"];
options.CallbackPath = new PathString("/purchasing/signin-oidc");
options.CookiePath = "/purchasing";
});

services.AddAuthentication().AddJwtBearer(
"cognito-provider",
options =>
{
options.Authority = appSettings.CognitoHost;
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = appSettings.CognitoHost,
ValidateAudience = false,
ValidAudience = appSettings.CognitoClientId,
ValidateLifetime = true,
ValidateIssuerSigningKey = true
};
options.MetadataAddress = $"{appSettings.CognitoHost}/.well-known/openid-configuration";
});

services.AddAuthentication(
options =>
{
options.DefaultScheme = "MultiAuth";
options.DefaultChallengeScheme = "MultiAuth";
}).AddScheme<MultiAuthOptions, MultiAuthHandler>(
"MultiAuth",
opts =>
{
opts.CognitoIssuer = appSettings.CognitoHost;
opts.CognitoScheme = "cognito-provider";
opts.LegacyScheme = LegacyJwtScheme;
});

services.AddAuthorization();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
Expand Down
15 changes: 15 additions & 0 deletions src/Service.Host/client/src/components/LoggedOut.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, { useEffect } from 'react';
import { signOutEntra } from '../helpers/userManager';

function LoggedOut() {
useEffect(() => {
signOutEntra();
// only want to run logout logic once on component mount, not on auth changes
// so ignore the linter this once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return <div>You are now logged out.</div>;
}

export default LoggedOut;
17 changes: 5 additions & 12 deletions src/Service.Host/client/src/components/Root.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import BomPrintReport from './reports/BomPrintReport';
import VendorManager from './VendorManager';
import VendorManagers from './VendorManagers';
import NavigationContainer from './containers/NavigationContainer';
import LoggedOut from './LoggedOut';

function Root({ store }) {
return (
Expand All @@ -113,9 +114,7 @@ function Root({ store }) {
<Provider store={store}>
<OidcProvider store={store} userManager={userManager}>
<>
<NavigationContainer
handleSignOut={() => userManager.signoutRedirect()}
/>
<NavigationContainer />

<HistoryRouter history={history}>
<Routes>
Expand All @@ -125,18 +124,12 @@ function Root({ store }) {
path="/"
element={<Navigate to="/purchasing" replace />}
/>
<Route
exact
path="/purchasing/signin-oidc-client"
element={<Callback />}
/>
<Route exact path="/purchasing/auth" element={<Callback />} />

<Route
exact
path="/accounts/signin-oidc-client"
element={<Callback />}
path="/purchasing/auth/logged-out"
element={<LoggedOut />}
/>

<Route
exact
path="/purchasing/suppliers"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import {
} from '@linn-it/linn-form-components-library';
import Navigation from '../common/Navigation';
import config from '../../config';
import { signOut } from '../../helpers/userManager';

const mapStateToProps = (state, ownProps) => {
const mapStateToProps = state => {
const myStuff = menuSelectors.getMyStuff(state);

// don't render the old sign out link
Expand All @@ -33,7 +34,7 @@ const mapStateToProps = (state, ownProps) => {
loading: menuSelectors.getMenuLoading(state),
seenNotifications: newsSelectors.getSeenNotifications(state),
unseenNotifications: newsSelectors.getUnseenNotifications(state),
handleSignOut: ownProps.handleSignOut
handleSignOut: signOut
};
};

Expand Down
62 changes: 50 additions & 12 deletions src/Service.Host/client/src/helpers/userManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,60 @@
import { WebStorageStateStore } from 'oidc-client';
import config from '../config';

const host = window.location.origin;
const authority = config.cognitoHost;
const clientId = config.cognitoClientId;
const domainPrefix = config.cognitoDomainPrefix;
const { origin } = window.location;

const oidcConfig = {
authority: config.authorityUri,
client_id: 'app2',
const redirectUri = `${origin}/purchasing/auth/`;
const logoutUri = `${origin}/purchasing/auth/logged-out`;

function getCognitoDomain(prefix, authorityUri) {
if (prefix && authorityUri) {
const regionMatch = authorityUri.match(/cognito-idp\.(.+)\.amazonaws\.com/);
const region = regionMatch ? regionMatch[1] : '';
return `https://${prefix}.auth.${region}.amazoncognito.com`;
}
return '';
}

const cognitoDomain = getCognitoDomain(domainPrefix, authority);

export const oidcConfig = {
authority,
client_id: clientId,
redirect_uri: redirectUri,
response_type: 'code',
scope: 'openid profile email associations offline_access',
redirect_uri: `${host}/purchasing/signin-oidc-client`,
post_logout_redirect_uri: `${host}`,
automaticSilentRenew: true,
filterProtocolClaims: true,
loadUserInfo: true,
monitorSession: false,
userStore: new WebStorageStateStore({ store: window.localStorage })
scope: 'email openid profile',
post_logout_redirect_uri: logoutUri,
userStore: new WebStorageStateStore({ store: window.localStorage }),
automaticSilentRenew: false
};

const userManager = createUserManager(oidcConfig);

export const signOut = () => {
if (!cognitoDomain) return;
window.location.href = `${cognitoDomain}/logout?client_id=${clientId}&logout_uri=${encodeURIComponent(
logoutUri
)}`;
};

export const signOutEntra = () => {
const { entraLogoutUri } = config;
window.location.href = `${entraLogoutUri}?post_logout_redirect_uri=${encodeURIComponent(
logoutUri
)}`;
};

userManager.signoutRedirect = async () => {
await userManager.removeUser();
signOut();
};

userManager.signOut = async () => {
await userManager.removeUser();
signOut();
};

export default userManager;
3 changes: 2 additions & 1 deletion src/Service.Host/client/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ function App() {

if (
(!user || user.expired) &&
window.location.pathname !== '/purchasing/signin-oidc-client'
window.location.pathname !== '/purchasing/auth/' &&
window.location.pathname !== '/purchasing/auth/logged-out'
) {
try {
await userManager.signinSilent();
Expand Down
10 changes: 7 additions & 3 deletions src/Service.Host/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
<div id="root"></div>
<script type="text/javascript">
window.APPLICATION_SETTINGS = {
authorityUri: 'https://www-sys.linn.co.uk/auth/',
appRoot: 'http://localhost:51699',
proxyRoot: 'https://app-sys.linn.co.uk'
authorityUri: 'https://www-sys.linn.co.uk/auth/',
appRoot: 'http://localhost:5050',
proxyRoot: 'https://app-sys.linn.co.uk',
cognitoHost: 'https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_A9JYwpcmh',
cognitoClientId: 'cnbggl0v65v1qna4p4lqsm64a',
cognitoDomainPrefix: 'linn-entra-auth-545349016803-sys',
entraLogoutUri: 'https://login.microsoftonline.com/2aebbc78-d071-43d4-b03e-7f539ab2f813/oauth2/v2.0/logout'
};
</script>
<script src="/purchasing/build/app.js"></script>
Expand Down
Loading