Skip to content

Commit

Permalink
Merge pull request #8 from guardian/feature/aazure-compatibility
Browse files Browse the repository at this point in the history
Feature/azure compatibility
  • Loading branch information
fredex42 authored Jun 14, 2022
2 parents 0573ce0 + 500d8ba commit 20553f8
Show file tree
Hide file tree
Showing 21 changed files with 3,362 additions and 2,878 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/npm-publish-github-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ jobs:
permissions:
contents: read
packages: write
checks: write
pull-requests: write

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/test-pull-requests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ on:
jobs:
runtests:
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
checks: write
pull-requests: write

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ Now, simply add the library as a dependency in your package.json:

Make sure that you use the latest version, as shown under Releases on the github front page.

When running locally, you will need to authenticate to the github npm repository like this:
```
npm login --scope=@guardian --registry=https://npm.pkg.github.com
npm notice Log in on https://npm.pkg.github.com/
Username: ([email protected]) github-user
Password:
Email: (this IS public) [email protected]
Logged in as github-user on https://npm.pkg.github.com/.
```

If you don't log in, yarn will complain:
```
error Couldn't find package "@guardian/pluto-headers" on the "npm" registry.
```

If you see this error:
```
error An unexpected error occurred: "https://npm.pkg.github.com/@guardian%2fpluto-headers: Your token has not been granted the required scopes to execute this query. The 'summary' field requires one of the following scopes: ['read:packages'], but your token has only been granted the: ['repo'] scopes. Please modify your token's scopes at: https://github.com/settings/tokens.".
```
Then it means that you need to go to the Tokens page on github (while logged in of course!) and add the `read_packages`scope.


*If you want to develop the library itself* see "Local Development" below.

**Help! I installed it locally and it broke my dev environment!** - see the section "Local Development" below.
Expand Down
109 changes: 84 additions & 25 deletions __tests__/components/AppSwitcher/TestLoginComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {JwtDataShape} from "../../../src";
import sinon from "sinon";
jest.mock("../../../src/utils/OAuth2Helper");
import {act} from "react-dom/test-utils";
import {OAuthContext} from "../../../src";
import {OAuthContextData, UserContext, UserContextProvider} from "../../../src";

describe("LoginComponent", ()=> {
let assignSpy:jest.SpyInstance<any,[string]>;
Expand All @@ -29,9 +31,20 @@ describe("LoginComponent", ()=> {
exp: 78910,
};

const rendered = mount(<LoginComponent loginData={mockLoginData}
onLoginExpired={loginExpiredCb}
tokenUri="https://fake-token-uri"/>);
const mockUserContext:UserContext = {
profile: mockLoginData,
updateProfile: ()=>{ }
}
const oauthconfig:OAuthContextData = {
clientId: "", oAuthUri: "", redirectUri: "", tokenUri: "https://fake-token-uri"
}

const rendered = mount(<OAuthContext.Provider value={oauthconfig} >
<UserContext.Provider value={mockUserContext}>
<LoginComponent onLoginExpired={loginExpiredCb}/>
</UserContext.Provider>
</OAuthContext.Provider>);

expect(setInterval).toHaveBeenCalledTimes(1);
expect(loginExpiredCb.callCount).toEqual(0);
});
Expand All @@ -48,25 +61,39 @@ describe("LoginComponent", ()=> {
const mockLoginData: JwtDataShape = {
aud: "my-audience",
iss: "my-idP",
iat: new Date().getTime() / 1000,
iat: 123456,
exp: 78910,
};

const rendered = mount(<LoginComponent loginData={mockLoginData}
onLoginExpired={loginExpiredCb}
onLoginRefreshed={loginRefreshedCb}
onLoggedOut={loggedOutCb}
onLoginCantRefresh={loginCantRefreshCb}
overrideRefreshLogin={mockRefresh}
tokenUri="https://fake-token-uri"/>);
const mockUserContext:UserContext = {
profile: mockLoginData,
updateProfile: ()=>{ }
}
const oauthconfig:OAuthContextData = {
clientId: "", oAuthUri: "", redirectUri: "", tokenUri: "https://fake-token-uri"
}

const rendered = mount(<OAuthContext.Provider value={oauthconfig} >
<UserContextProvider value={mockUserContext}>
<LoginComponent onLoginExpired={loginExpiredCb}
onLoginRefreshed={loginRefreshedCb}
onLoggedOut={loggedOutCb}
onLoginCantRefresh={loginCantRefreshCb}
overrideRefreshLogin={mockRefresh}
/>
</UserContextProvider>
</OAuthContext.Provider>);

expect(rendered.find("#refresh-in-progress").length).toEqual(0);
expect(rendered.find("#refresh-failed").length).toEqual(0);
expect(rendered.find("#refresh-success").length).toEqual(0);

act(() => {
await act(() => {
rendered.update();
jest.advanceTimersByTime(60001);
Promise.resolve();
});

expect(mockRefresh.calledOnceWith("https://fake-token-uri")).toBeTruthy();
await act(()=>Promise.resolve()); //this allows other outstanding promises to resolve _first_, including the one that
//sets the component state and calls loginRefreshedCb
Expand All @@ -86,7 +113,6 @@ describe("LoginComponent", ()=> {
});

it("should fire a callback and display a message if the refresh failed", async ()=>{
// jest.useFakeTimers();
const loginExpiredCb = sinon.spy();
const loginRefreshedCb = sinon.spy();
const loggedOutCb = sinon.spy();
Expand All @@ -102,13 +128,24 @@ describe("LoginComponent", ()=> {
exp: (new Date().getTime() / 1000)+30,
};

const rendered = mount(<LoginComponent loginData={mockLoginData}
onLoginExpired={loginExpiredCb}

const mockUserContext:UserContext = {
profile: mockLoginData,
updateProfile: ()=>{ }
}
const oauthconfig:OAuthContextData = {
clientId: "", oAuthUri: "", redirectUri: "", tokenUri: "https://fake-token-uri"
}

const rendered = mount(<OAuthContext.Provider value={oauthconfig} >
<UserContextProvider value={mockUserContext}>
<LoginComponent onLoginExpired={loginExpiredCb}
onLoginRefreshed={loginRefreshedCb}
onLoggedOut={loggedOutCb}
onLoginCantRefresh={loginCantRefreshCb}
overrideRefreshLogin={mockRefresh}
tokenUri="https://fake-token-uri"/>);
overrideRefreshLogin={mockRefresh}/>
</UserContextProvider>
</OAuthContext.Provider>);

expect(rendered.find("#refresh-in-progress").length).toEqual(0);
expect(rendered.find("#refresh-failed").length).toEqual(0);
Expand Down Expand Up @@ -149,13 +186,24 @@ describe("LoginComponent", ()=> {
exp: (new Date().getTime() / 1000)-10,
};

const rendered = mount(<LoginComponent loginData={mockLoginData}
onLoginExpired={loginExpiredCb}

const mockUserContext:UserContext = {
profile: mockLoginData,
updateProfile: ()=>{ }
}
const oauthconfig:OAuthContextData = {
clientId: "", oAuthUri: "", redirectUri: "", tokenUri: "https://fake-token-uri"
}

const rendered = mount(<OAuthContext.Provider value={oauthconfig} >
<UserContextProvider value={mockUserContext}>
<LoginComponent onLoginExpired={loginExpiredCb}
onLoginRefreshed={loginRefreshedCb}
onLoggedOut={loggedOutCb}
onLoginCantRefresh={loginCantRefreshCb}
overrideRefreshLogin={mockRefresh}
tokenUri="https://fake-token-uri"/>);
overrideRefreshLogin={mockRefresh}/>
</UserContextProvider>
</OAuthContext.Provider>);

expect(rendered.find("#refresh-in-progress").length).toEqual(0);
expect(rendered.find("#refresh-failed").length).toEqual(0);
Expand Down Expand Up @@ -200,13 +248,24 @@ describe("LoginComponent", ()=> {
exp: (new Date().getTime() / 1000)-10,
};

const rendered = mount(<LoginComponent loginData={mockLoginData}
onLoginExpired={loginExpiredCb}

const mockUserContext:UserContext = {
profile: mockLoginData,
updateProfile: ()=>{ }
}
const oauthconfig:OAuthContextData = {
clientId: "", oAuthUri: "", redirectUri: "", tokenUri: "https://fake-token-uri"
}

const rendered = mount(<OAuthContext.Provider value={oauthconfig} >
<UserContextProvider value={mockUserContext}>
<LoginComponent onLoginExpired={loginExpiredCb}
onLoginRefreshed={loginRefreshedCb}
onLoggedOut={loggedOutCb}
onLoginCantRefresh={loginCantRefreshCb}
overrideRefreshLogin={mockRefresh}
tokenUri="https://fake-token-uri"/>);
overrideRefreshLogin={mockRefresh}/>
</UserContextProvider>
</OAuthContext.Provider>);

const btn = rendered.find("button.login-button");
expect(btn.length).toEqual(1);
Expand Down
46 changes: 42 additions & 4 deletions __tests__/components/Context/TestOAuthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {useContext} from "react";
import fetchMock from "jest-fetch-mock";
import {OAuthContext, OAuthContextProvider} from "../../../src";
import {generateCodeChallenge, OAuthContext, OAuthContextProvider} from "../../../src/components/Context/OAuthContext";
import {mount} from "enzyme";
import {act} from "react-dom/test-utils";
import sinon from "sinon";
Expand Down Expand Up @@ -35,7 +35,8 @@ describe("OAuthContextProvider", ()=>{
clientId: "some-client",
tokenUri: "some-token",
oAuthUri: "some-uri",
resource: "some-resource"
resource: "some-resource",
adminClaimName: "iamyourfather"
};

const errorCb = sinon.spy();
Expand Down Expand Up @@ -96,6 +97,43 @@ describe("makeLoginUrl", ()=>{
};

const result = makeLoginUrl(sampleData);
expect(result).toEqual("some-oauth-uri?response_type=code&client_id=some-client&resource=some-resource&redirect_uri=https%3A%2F%2Fsome-redirect%2Furi&state=%2F");
})
const removeRandomPart = /code_challenge=[a-fA-F0-9]{16,}&/;
const resultToTest = result.replace(removeRandomPart, "");

expect(resultToTest).toEqual("some-oauth-uri?response_type=code&client_id=some-client&redirect_uri=https%3A%2F%2Fsome-redirect%2Furi&state=%2F&resource=some-resource");
expect(removeRandomPart.test(result)).toBeTruthy();
});

it("should not include the resource parameter if it's not given", ()=>{
const sampleData:OAuthContextData = {
clientId: "some-client",
oAuthUri: "some-oauth-uri",
tokenUri: "some-token-uri",
redirectUri: "https://some-redirect/uri"
};

const result = makeLoginUrl(sampleData);
const removeRandomPart = /&code_challenge=[a-fA-F0-9]{16,}/;
const resultToTest = result.replace(removeRandomPart, "");

expect(resultToTest).toEqual("some-oauth-uri?response_type=code&client_id=some-client&redirect_uri=https%3A%2F%2Fsome-redirect%2Furi&state=%2F");
expect(removeRandomPart.test(result)).toBeTruthy();
});

it("should include the scope parameter if it is given", ()=>{
const sampleData:OAuthContextData = {
clientId: "some-client",
oAuthUri: "some-oauth-uri",
scope: "https://graph.microsoft.com/openid",
tokenUri: "some-token-uri",
redirectUri: "https://some-redirect/uri"
};

const result = makeLoginUrl(sampleData);
const removeRandomPart = /code_challenge=[a-fA-F0-9]{16,}&/;
const resultToTest = result.replace(removeRandomPart, "");

expect(resultToTest).toEqual("some-oauth-uri?response_type=code&client_id=some-client&redirect_uri=https%3A%2F%2Fsome-redirect%2Furi&state=%2F&scope=https%3A%2F%2Fgraph.microsoft.com%2Fopenid");
expect(removeRandomPart.test(result)).toBeTruthy();
});
})
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { validateAndDecode } from "../../src";
import { verifyJwt } from "../../src";
import {OAuthContextData} from "../../src";
import fetchMock from "jest-fetch-mock";

describe("validateAndDecode", () => {
beforeEach(()=>{
localStorage.clear();
fetchMock.resetMocks();
});

it("should decode an example jwt", (done) => {
const exampleToken =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiOiJqb2huX2RvZSIsImZhbWlseV9uYW1lIjoiRG9lIiwiZmlyc3RfbmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9.IfSuq8z7BL6DQIydiK5fEC85z9t_twQTQj0rfTpMXPA";
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
//can get them from https://jwt.io/
validateAndDecode(exampleToken, "your-256-bit-secret")
const oauthconfig:OAuthContextData = {
clientId: "", oAuthUri: "", redirectUri: "", tokenUri: ""
}
fetchMock.mockResponseOnce("your-256-bit-secret")
verifyJwt(oauthconfig, exampleToken)
.then((decodedContent) => {
expect(decodedContent.sub).toEqual("1234567890");
expect(decodedContent.username).toEqual("john_doe");
expect(decodedContent.family_name).toEqual("Doe");
expect(decodedContent.first_name).toEqual("John");
expect(decodedContent.name).toEqual("John Doe");
expect(decodedContent.iat).toEqual(1516239022);

done();
Expand All @@ -25,7 +34,11 @@ describe("validateAndDecode", () => {
const exampleToken =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiOiJqb2huX2RvZSIsImZhbWlseV9uYW1lIjoiRG9lIiwiZmlyc3RfbmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9.IfSuq8z7BL6DQIydiK5fEC85z9t_twQTQj0rfTpMXPa";
//can get them from https://jwt.io/
validateAndDecode(exampleToken, "your-256-bit-secret")
const oauthconfig:OAuthContextData = {
clientId: "", oAuthUri: "", redirectUri: "", tokenUri: ""
}
fetchMock.mockResponseOnce("your-256-bit-secret");
verifyJwt(oauthconfig, exampleToken)
.then((decodedContent) => {
console.log(decodedContent);

Expand Down
Loading

0 comments on commit 20553f8

Please sign in to comment.