|
| 1 | +const jwtDecode = require('jsonwebtoken/decode'); |
| 2 | +const { Issuer, Strategy: OpenIDStrategy } = require('openid-client'); |
| 3 | +const mongoose = require('mongoose'); |
| 4 | +const { MongoMemoryServer } = require('mongodb-memory-server'); |
| 5 | +const User = require('~/models/User'); |
| 6 | +const setupOpenId = require('./openidStrategy'); |
| 7 | + |
| 8 | +jest.mock('jsonwebtoken/decode'); |
| 9 | +jest.mock('openid-client'); |
| 10 | + |
| 11 | +jest.mock('~/server/services/Files/strategies', () => ({ |
| 12 | + getStrategyFunctions: jest.fn(() => ({ |
| 13 | + saveBuffer: jest.fn(), |
| 14 | + })), |
| 15 | +})); |
| 16 | + |
| 17 | +Issuer.discover = jest.fn().mockResolvedValue({ |
| 18 | + Client: jest.fn(), |
| 19 | +}); |
| 20 | + |
| 21 | +describe('setupOpenId', () => { |
| 22 | + const OLD_ENV = process.env; |
| 23 | + describe('OpenIDStrategy', () => { |
| 24 | + let validateFn, mongoServer; |
| 25 | + |
| 26 | + beforeAll(async () => { |
| 27 | + mongoServer = await MongoMemoryServer.create(); |
| 28 | + const mongoUri = mongoServer.getUri(); |
| 29 | + await mongoose.connect(mongoUri); |
| 30 | + }); |
| 31 | + |
| 32 | + afterAll(async () => { |
| 33 | + process.env = OLD_ENV; |
| 34 | + await mongoose.disconnect(); |
| 35 | + await mongoServer.stop(); |
| 36 | + }); |
| 37 | + |
| 38 | + beforeEach(async () => { |
| 39 | + jest.clearAllMocks(); |
| 40 | + await User.deleteMany({}); |
| 41 | + process.env = { |
| 42 | + ...OLD_ENV, |
| 43 | + OPENID_ISSUER: 'https://fake-issuer.com', |
| 44 | + OPENID_CLIENT_ID: 'fake_client_id', |
| 45 | + OPENID_CLIENT_SECRET: 'fake_client_secret', |
| 46 | + DOMAIN_SERVER: 'https://example.com', |
| 47 | + OPENID_CALLBACK_URL: '/callback', |
| 48 | + OPENID_SCOPE: 'openid profile email', |
| 49 | + OPENID_REQUIRED_ROLE: 'requiredRole', |
| 50 | + OPENID_REQUIRED_ROLE_PARAMETER_PATH: 'roles', |
| 51 | + OPENID_REQUIRED_ROLE_TOKEN_KIND: 'id', |
| 52 | + }; |
| 53 | + |
| 54 | + jwtDecode.mockReturnValue({ |
| 55 | + roles: ['requiredRole'], |
| 56 | + }); |
| 57 | + |
| 58 | + //call setup so we can grab a reference to the validate function |
| 59 | + await setupOpenId(); |
| 60 | + validateFn = OpenIDStrategy.mock.calls[0][1]; |
| 61 | + }); |
| 62 | + |
| 63 | + const tokenset = { |
| 64 | + id_token: 'fake_id_token', |
| 65 | + }; |
| 66 | + |
| 67 | + const userinfo = { |
| 68 | + sub: '1234', |
| 69 | + |
| 70 | + email_verified: true, |
| 71 | + given_name: 'First', |
| 72 | + family_name: 'Last', |
| 73 | + name: 'My Full', |
| 74 | + username: 'flast', |
| 75 | + }; |
| 76 | + |
| 77 | + const userModel = { |
| 78 | + openidId: userinfo.sub, |
| 79 | + email: userinfo.email, |
| 80 | + }; |
| 81 | + |
| 82 | + it('should set username correctly for a new user when username claim exists', async () => { |
| 83 | + const expectUsername = userinfo.username.toLowerCase(); |
| 84 | + await validateFn(tokenset, userinfo, (err, user) => { |
| 85 | + expect(err).toBe(null); |
| 86 | + expect(user.username).toBe(expectUsername); |
| 87 | + }); |
| 88 | + |
| 89 | + await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull(); |
| 90 | + }); |
| 91 | + |
| 92 | + it('should set username correctly for a new user when given_name claim exists, but username does not', async () => { |
| 93 | + let userinfo_modified = { ...userinfo }; |
| 94 | + delete userinfo_modified.username; |
| 95 | + const expectUsername = userinfo.given_name.toLowerCase(); |
| 96 | + |
| 97 | + await validateFn(tokenset, userinfo_modified, (err, user) => { |
| 98 | + expect(err).toBe(null); |
| 99 | + expect(user.username).toBe(expectUsername); |
| 100 | + }); |
| 101 | + await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull(); |
| 102 | + }); |
| 103 | + |
| 104 | + it('should set username correctly for a new user when email claim exists, but username and given_name do not', async () => { |
| 105 | + let userinfo_modified = { ...userinfo }; |
| 106 | + delete userinfo_modified.username; |
| 107 | + delete userinfo_modified.given_name; |
| 108 | + const expectUsername = userinfo.email.toLowerCase(); |
| 109 | + |
| 110 | + await validateFn(tokenset, userinfo_modified, (err, user) => { |
| 111 | + expect(err).toBe(null); |
| 112 | + expect(user.username).toBe(expectUsername); |
| 113 | + }); |
| 114 | + await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull(); |
| 115 | + }); |
| 116 | + |
| 117 | + it('should set username correctly for a new user when using OPENID_USERNAME_CLAIM', async () => { |
| 118 | + process.env.OPENID_USERNAME_CLAIM = 'sub'; |
| 119 | + const expectUsername = userinfo.sub.toLowerCase(); |
| 120 | + |
| 121 | + await validateFn(tokenset, userinfo, (err, user) => { |
| 122 | + expect(err).toBe(null); |
| 123 | + expect(user.username).toBe(expectUsername); |
| 124 | + }); |
| 125 | + await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull(); |
| 126 | + }); |
| 127 | + |
| 128 | + it('should set name correctly for a new user with first and last names', async () => { |
| 129 | + const expectName = userinfo.given_name + ' ' + userinfo.family_name; |
| 130 | + await validateFn(tokenset, userinfo, (err, user) => { |
| 131 | + expect(err).toBe(null); |
| 132 | + expect(user.name).toBe(expectName); |
| 133 | + }); |
| 134 | + await expect(User.exists({ name: expectName })).resolves.not.toBeNull(); |
| 135 | + }); |
| 136 | + |
| 137 | + it('should set name correctly for a new user using OPENID_NAME_CLAIM', async () => { |
| 138 | + const expectName = 'Custom Name'; |
| 139 | + process.env.OPENID_NAME_CLAIM = 'name'; |
| 140 | + let userinfo_modified = { ...userinfo, name: expectName }; |
| 141 | + |
| 142 | + await validateFn(tokenset, userinfo_modified, (err, user) => { |
| 143 | + expect(err).toBe(null); |
| 144 | + expect(user.name).toBe(expectName); |
| 145 | + }); |
| 146 | + await expect(User.exists({ name: expectName })).resolves.not.toBeNull(); |
| 147 | + }); |
| 148 | + |
| 149 | + it('should should update existing user after login', async () => { |
| 150 | + const expectUsername = userinfo.username.toLowerCase(); |
| 151 | + await User.create(userModel); |
| 152 | + |
| 153 | + await validateFn(tokenset, userinfo, (err) => { |
| 154 | + expect(err).toBe(null); |
| 155 | + }); |
| 156 | + const newUser = await User.findOne({ openidId: userModel.openidId }); |
| 157 | + await expect(newUser.provider).toBe('openid'); |
| 158 | + await expect(newUser.username).toBe(expectUsername); |
| 159 | + await expect(newUser.name).toBe(userinfo.given_name + ' ' + userinfo.family_name); |
| 160 | + }); |
| 161 | + |
| 162 | + it('should should enforce required role', async () => { |
| 163 | + jwtDecode.mockReturnValue({ |
| 164 | + roles: ['SomeOtherRole'], |
| 165 | + }); |
| 166 | + await validateFn(tokenset, userinfo, (err, user, details) => { |
| 167 | + expect(err).toBe(null); |
| 168 | + expect(user).toBe(false); |
| 169 | + expect(details.message).toBe( |
| 170 | + 'You must have the "' + process.env.OPENID_REQUIRED_ROLE + '" role to log in.', |
| 171 | + ); |
| 172 | + }); |
| 173 | + }); |
| 174 | + }); |
| 175 | +}); |
0 commit comments