-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add keycloak authentication closes #1634 #1716
base: main
Are you sure you want to change the base?
Changes from 4 commits
b3fbcf1
416060c
d337b9b
a7a8a13
d58953c
89f03a5
a1ea1d1
52e9cc0
2ae3800
ad44bc4
2723eae
26b9eaa
7f3ebd1
22a76e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are a few eslint warnings that need addressing. I'd recommend installing an ESLint plugin for your IDE, like this one for VS Code https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import c from 'config' | ||
import { UserInformation } from '../connectors/authentication/Base.js' | ||
import config from '../utils/config.js' | ||
import { ConfigurationError, InternalError } from '../utils/error.js' | ||
|
||
export async function listUsers(query: string, exactMatch = false) { | ||
let dnName: string | ||
let realm: string | ||
try { | ||
if (!config?.oauth?.keycloak) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For readability, I would recommend moving this if statement to just before the try statement. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. both this an configurationerror have been fixed, just missed them on a proof read |
||
throw ConfigurationError('OAuth Keycloak configuration is missing') | ||
} | ||
realm = config.oauth.keycloak.realm | ||
} catch (e) { | ||
throw ConfigurationError('Cannot find userIdAttribute in oauth configuration', { oauthConfiguration: config?.oauth }) | ||
onecrazygenius marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
const token = await getKeycloakToken() | ||
|
||
const filter = exactMatch ? `${query}` : `${query}*` | ||
const url = `${config.oauth.keycloak.serverUrl}/admin/realms/${realm}/users?search=${filter}` | ||
|
||
let results | ||
try { | ||
results = await fetch(url, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. utilised node-fetch and added types of Response to results |
||
method: 'GET', | ||
headers: { | ||
Authorization: `Bearer ${token}` | ||
} | ||
}) | ||
} catch (err) { | ||
throw InternalError('Error when querying Keycloak for users.', { err }) | ||
} | ||
const resultsData = await results.json() as any[] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would recommend using a type guard here rather than using a type assertion with an array of type There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See here for an example https://github.com/gchq/Bailo/blob/main/backend/src/clients/registry.ts#L61 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added in new commit |
||
console.log(resultsData) | ||
onecrazygenius marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!resultsData || resultsData.length === 0) { | ||
return [] | ||
} | ||
|
||
const initialValue: Array<UserInformation & { dn: string }> = [] | ||
const users = resultsData.reduce((acc, keycloakUser) => { | ||
const dn = keycloakUser.id // Assuming 'id' is the dnName | ||
if (!dn) { | ||
return acc | ||
} | ||
const email = keycloakUser.email | ||
const name = keycloakUser.firstName | ||
const info: UserInformation = { | ||
...(email && { email }), | ||
...(name && { name }), | ||
} | ||
acc.push({ ...info, dn }) | ||
return acc | ||
}, initialValue) | ||
return users | ||
} | ||
|
||
async function getKeycloakToken() { | ||
if (!config?.oauth?.keycloak) { | ||
throw ConfigurationError('OAuth Keycloak configuration is missing') | ||
} | ||
const url = `${config.oauth.keycloak.serverUrl}/realms/${config.oauth.keycloak.realm}/protocol/openid-connect/token` | ||
const params = new URLSearchParams() | ||
params.append('client_id', config.oauth.keycloak.clientId) | ||
params.append('client_secret', config.oauth.keycloak.clientSecret) | ||
params.append('grant_type', 'client_credentials') | ||
|
||
try { | ||
const response = await fetch(url, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded' | ||
}, | ||
body: params | ||
}) | ||
const data = await response.json() as { access_token: string } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See other comment about using type guards rather type assertions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added as above let me know if anything else needed |
||
if (!data.access_token) { | ||
throw InternalError('Access token is missing in the response', { response: data }) | ||
} | ||
return data.access_token | ||
} catch (err) { | ||
throw InternalError('Error when obtaining Keycloak token.', { err }) | ||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you consider having two separate connectors, one for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this bullet point you've included in an earlier comment summarises the drawback in have a single connector like you have here "enabling a toggle system based on the config for the application to load the correct provider". The connector design itself is a toggle and so your implementation is adding a toggle within a toggle. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the reasoning for going with this implementation is then each client type say you had a google oauth or azure entra is a future version, the flexibility provided comes from each client has a config and then the main oauth connector acts as a 'resolver'. So this implementation seemed to fit with the existing design to an extent with code seperated for the client and connector type The challenge of splitting the connectors/clients completely apart would probably be duplication of code despite simplifying the configuration. I was aiming to take inspiration from next-auth where you have a general OAuth Connector and then configure your client to use for that OAuth connection. e.g? https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/index.ts I think your lead on which way to implement is obviously best as I want to alter your codebase, so for the rest of comments for now I'll fix with the current implementation where possible but we can look at how this get structured. Either way is a trade of in flexibility at either a config or code level really. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi, apologies for the delayed response and thank you for your reply. For maintainability going forward I still think it would be best to have separate connectors for Keycloak and Cognito. I think this would simplify the configuration and simplify the implementation due to not needing to have conditionals for every provider that gets added going forward in a single class. I do agree though agree with your concern about duplication so I'd suggest having an abstract oauth base class that contains common oauth code. This would be the existing |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please update the helm chart configuration with these changes here https://github.com/gchq/Bailo/blob/main/infrastructure/helm/bailo/templates/bailo/bailo.configmap.yaml and the values file. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated chart with configmap and values changes. |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -114,11 +114,17 @@ export interface Config { | |||
oauth: { | ||||
provider: string | ||||
grant: grant.GrantConfig | grant.GrantOptions | ||||
cognito: { | ||||
cognito?: { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having optional blocks of configuration isn't something we've decided to do for things like this previously as you can see from the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added my thoughts around this to the top comment, let me know what you would prefer and I'm happy to add around this as needed |
||||
identityProviderClient: { region: string; credentials: { accessKeyId: string; secretAccessKey: string } } | ||||
userPoolId: string | ||||
userIdAttribute: string | ||||
} | ||||
keycloak?: { | ||||
realm: string | ||||
clientId: string | ||||
clientSecret: string | ||||
serverUrl: string | ||||
} | ||||
} | ||||
|
||||
defaultSchemas: { | ||||
|
@@ -172,4 +178,12 @@ export interface Config { | |||
} | ||||
|
||||
const config: Config = _config.util.toObject() | ||||
|
||||
if (config.oauth && | ||||
!config.oauth.keycloak && | ||||
!config.oauth.cognito | ||||
) { | ||||
throw new Error('If OAuth is configured, either Keycloak or Cognito configuration must be provided.') | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This error type would be well suited here Bailo/backend/src/utils/error.ts Line 55 in 7b4b784
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. altered |
||||
} | ||||
|
||||
export default deepFreeze(config) as Config |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please include unit tests