Skip to content

Commit

Permalink
Merge pull request #530 from bandada-infra/feat/many-credentials
Browse files Browse the repository at this point in the history
Support Groups with multiple credentials and logical operators
  • Loading branch information
vplasencia authored Jul 2, 2024
2 parents a1062b4 + 74e7874 commit e366f5c
Show file tree
Hide file tree
Showing 11 changed files with 1,237 additions and 78 deletions.
80 changes: 66 additions & 14 deletions apps/api/src/app/credentials/credentials.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jest.mock("@bandada/credentials", () => ({
getJsonRpcProvider: jest.fn()
})),
validateCredentials: jest.fn(() => true),
validateManyCredentials: jest.fn(() => true),
providers: [{ name: "twitter" }, { name: "github" }, { name: "blockchain" }]
}))

Expand Down Expand Up @@ -155,7 +156,7 @@ describe("CredentialsService", () => {
providerName: "github"
})

await credentialsService.addMember(_stateId, "code")
await credentialsService.addMember([_stateId], ["code"])

const fun = credentialsService.setOAuthState({
groupId,
Expand All @@ -171,7 +172,7 @@ describe("CredentialsService", () => {

describe("# addMember", () => {
it("Should throw an error if the OAuth does not exist", async () => {
const fun = credentialsService.addMember("123", "code")
const fun = credentialsService.addMember(["123"], ["code"])

await expect(fun).rejects.toThrow(`OAuth state does not exist`)
})
Expand All @@ -184,8 +185,8 @@ describe("CredentialsService", () => {
})

const clientRedirectUri = await credentialsService.addMember(
_stateId,
"code"
[_stateId],
["code"]
)

expect(clientRedirectUri).toBeUndefined()
Expand Down Expand Up @@ -231,15 +232,15 @@ describe("CredentialsService", () => {
)

const clientRedirectUri1 = await credentialsService.addMember(
_stateId1,
"code"
[_stateId1],
["code"]
)

expect(clientRedirectUri1).toBeUndefined()

const clientRedirectUri2 = await credentialsService.addMember(
_stateId2,
"code"
[_stateId2],
["code"]
)

expect(clientRedirectUri2).toBeUndefined()
Expand All @@ -265,7 +266,7 @@ describe("CredentialsService", () => {
providerName: "github"
})

const fun = credentialsService.addMember(_stateId, "code")
const fun = credentialsService.addMember([_stateId], ["code"])

await expect(fun).rejects.toThrow(
`OAuth account does not match criteria`
Expand All @@ -289,7 +290,7 @@ describe("CredentialsService", () => {
providerName: "github"
})

const fun = credentialsService.addMember(_stateId, "code")
const fun = credentialsService.addMember([_stateId], ["code"])

await expect(fun).rejects.toThrow(
`OAuth account has already joined the group`
Expand Down Expand Up @@ -323,9 +324,9 @@ describe("CredentialsService", () => {
})

const clientRedirectUri = await credentialsService.addMember(
_stateId,
[_stateId],
undefined,
"0x"
["0x"]
)

expect(clientRedirectUri).toBeUndefined()
Expand All @@ -339,9 +340,60 @@ describe("CredentialsService", () => {
})

const clientRedirectUri = await credentialsService.addMember(
_stateId,
[_stateId],
undefined,
"0x1"
["0x1"]
)

expect(clientRedirectUri).toBeUndefined()
})
it("Should add a member to a group with many credentials", async () => {
const { id } = await groupsService.createGroup(
{
name: "Group3",
description: "This is a description",
treeDepth: 16,
fingerprintDuration: 3600,
credentials: JSON.stringify({
credentials: [
{
id: "BLOCKCHAIN_TRANSACTIONS",
criteria: {
minTransactions: 12,
network: "sepolia"
}
},
{
id: "GITHUB_FOLLOWERS",
criteria: {
minFollowers: 5
}
}
],
expression: ["", "and", ""]
})
},
"admin"
)

groupId = id

const _stateId1 = await credentialsService.setOAuthState({
groupId,
memberId: "1",
providerName: "blockchain"
})

const _stateId2 = await credentialsService.setOAuthState({
groupId,
memberId: "1",
providerName: "github"
})

const clientRedirectUri = await credentialsService.addMember(
[_stateId1, _stateId2],
["code"],
["0x"]
)

expect(clientRedirectUri).toBeUndefined()
Expand Down
176 changes: 166 additions & 10 deletions apps/api/src/app/credentials/credentials.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
validateCredentials,
validateManyCredentials,
getProvider,
BlockchainProvider,
Web2Provider,
Expand Down Expand Up @@ -82,23 +83,178 @@ export class CredentialsService {
* @returns Redirect URI
*/
async addMember(
oAuthState: string,
oAuthCode?: string,
address?: string
oAuthState: string[],
oAuthCode?: string[],
address?: string[]
): Promise<string> {
if (!this.oAuthState.has(oAuthState)) {
if (!this.oAuthState.has(oAuthState[0])) {
throw new BadRequestException(`OAuth state does not exist`)
}

const {
let {
groupId,
memberId,
providerName,
redirectUri: clientRedirectUri
} = this.oAuthState.get(oAuthState)
} = this.oAuthState.get(oAuthState[0])

const group = await this.groupsService.getGroup(groupId)

if (group.credentials === null) return

const credentials = JSON.parse(group.credentials)

if (
credentials.credentials !== undefined &&
credentials.expression !== undefined
) {
// The group has multiple credentials

const contexts = []
let accountHash: string

for (let i = 0; i < credentials.credentials.length; i += 1) {
// Get the name of the provider of the credential
const credentialProvider = credentials.credentials[i].id
.split("_")[0]
.toLowerCase()

// Find the OAuthState for this credential provider
const credentialOAuthState = oAuthState.find(
(s) =>
this.oAuthState.get(s).providerName ===
credentialProvider
)

if (credentialOAuthState === undefined) {
return
}

// Get the oAuthState data for this credential
;({
groupId,
memberId,
providerName,
redirectUri: clientRedirectUri
} = this.oAuthState.get(credentialOAuthState))
const provider = getProvider(providerName)

let context: Web2Context | BlockchainContext

if (address && credentialProvider === "blockchain") {
const { network } = credentials.credentials[i].criteria

const supportedNetwork =
blockchainCredentialSupportedNetworks.find(
(n) =>
n.name.toLowerCase() === network.toLowerCase()
)

if (supportedNetwork === undefined)
throw new BadRequestException(
`The network is not supported`
)

const networkEnvVariableName =
supportedNetwork.id.toUpperCase()

const web3providerRpcURL =
process.env[`${networkEnvVariableName}_RPC_URL`]

// eslint-disable-next-line no-await-in-loop
const jsonRpcProvider = await (
provider as BlockchainProvider
).getJsonRpcProvider(web3providerRpcURL)

context = {
address: address[0],
jsonRpcProvider
}

// Check if the same account has already joined the group.
accountHash = id(address + groupId)
} else {
const clientId =
process.env[`${providerName.toUpperCase()}_CLIENT_ID`]
const clientSecret =
process.env[
`${providerName.toUpperCase()}_CLIENT_SECRET`
]
const redirectUri =
process.env[
`${providerName.toUpperCase()}_REDIRECT_URI`
]

// Exchange the OAuth code for a valid access token.
// eslint-disable-next-line no-await-in-loop
const accessToken = await (
provider as Web2Provider
).getAccessToken(
clientId,
clientSecret,
oAuthCode[0],
oAuthState[0],
redirectUri
)

// eslint-disable-next-line no-await-in-loop
const profile = await (provider as Web2Provider).getProfile(
accessToken
)

context = {
profile,
accessTokens: { [providerName]: accessToken }
}

// Check if the same account has already joined the group.
accountHash = id(profile.id + groupId)
}

if (
group.oAuthAccounts.find(
// eslint-disable-next-line @typescript-eslint/no-loop-func
(account) => account.accountHash === accountHash
)
) {
throw new BadRequestException(
"OAuth account has already joined the group"
)
}

contexts.push(context)
}

// Check credentials.
if (
!(await validateManyCredentials(
credentials.credentials,
contexts,
credentials.expression
))
) {
throw new UnauthorizedException(
"OAuth account does not match criteria"
)
}

// Save OAuth account to prevent the same account to join groups with
// different member ids.
const oAuthAccount = new OAuthAccount()

oAuthAccount.group = group
oAuthAccount.accountHash = accountHash

await this.oAuthAccountRepository.save(oAuthAccount)
await this.groupsService.addMember(groupId, memberId)

for (let i = 0; i < oAuthState.length; i += 1) {
this.oAuthState.delete(oAuthState[i])
}

return clientRedirectUri
}

const provider = getProvider(providerName)

let accountHash: string
Expand All @@ -125,7 +281,7 @@ export class CredentialsService {
).getJsonRpcProvider(web3providerRpcURL)

context = {
address,
address: address[0],
jsonRpcProvider
}

Expand All @@ -143,8 +299,8 @@ export class CredentialsService {
const accessToken = await (provider as Web2Provider).getAccessToken(
clientId,
clientSecret,
oAuthCode,
oAuthState,
oAuthCode[0],
oAuthState[0],
redirectUri
)

Expand Down Expand Up @@ -190,7 +346,7 @@ export class CredentialsService {
await this.oAuthAccountRepository.save(oAuthAccount)
await this.groupsService.addMember(groupId, memberId)

this.oAuthState.delete(oAuthState)
this.oAuthState.delete(oAuthState[0])

return clientRedirectUri
}
Expand Down
17 changes: 10 additions & 7 deletions apps/api/src/app/credentials/dto/add-member.dto.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { IsString, IsOptional } from "class-validator"
import { IsArray, ArrayNotEmpty, IsOptional } from "class-validator"

export class AddMemberDto {
@IsString()
readonly oAuthState: string
@IsArray()
@ArrayNotEmpty()
readonly oAuthState: string[]

@IsOptional()
@IsString()
readonly oAuthCode: string
@IsArray()
@ArrayNotEmpty()
readonly oAuthCode: string[]

@IsOptional()
@IsString()
readonly address: string
@IsArray()
@ArrayNotEmpty()
readonly address: string[]
}
Loading

0 comments on commit e366f5c

Please sign in to comment.