Skip to content

Commit

Permalink
Add apikey and client generation feature #156
Browse files Browse the repository at this point in the history
  • Loading branch information
dewmini committed Mar 8, 2023
1 parent 3764e9b commit de139bb
Show file tree
Hide file tree
Showing 11 changed files with 386 additions and 4 deletions.
3 changes: 3 additions & 0 deletions userdetails-cognito/grails-app/conf/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,6 @@ account:
MFAenabled: true
authorised-systems:
edit-enabled: false
oauth.support.dynamic.client.registration: true
oauth.support.dynamic.client.scopes: ["email", "openid", "profile", "ala/attrs" , "ala/roles"]
tokenApp.tokenGeneration.url: https://tokens-cognito-support.dev.ala.org.au?step=generation
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import au.org.ala.web.OidcClientProperties
import au.org.ala.ws.security.JwtProperties
import au.org.ala.ws.tokens.TokenService
import com.amazonaws.auth.*
import com.amazonaws.services.apigateway.AmazonApiGateway
import com.amazonaws.services.apigateway.AmazonApiGatewayClientBuilder
import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProvider
import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProviderClient
import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProviderClientBuilder
import grails.boot.GrailsApp
import grails.boot.config.GrailsAutoConfiguration
Expand Down Expand Up @@ -69,8 +70,21 @@ class Application extends GrailsAutoConfiguration {
return cognitoIdp
}

@Bean
AmazonApiGateway gatewayIdpClient(AWSCredentialsProvider awsCredentialsProvider) {
def region = grailsApplication.config.getProperty('cognito.region')

AmazonApiGateway gatewayIdp = AmazonApiGatewayClientBuilder.standard()
.withRegion(region)
.withCredentials(awsCredentialsProvider)
.build()

return gatewayIdp
}

@Bean('userService')
IUserService userService(TokenService tokenService, EmailService emailService, AWSCognitoIdentityProvider cognitoIdp, JwtProperties jwtProperties) {
IUserService userService(TokenService tokenService, EmailService emailService, AWSCognitoIdentityProvider cognitoIdp, JwtProperties jwtProperties,
AmazonApiGateway gatewayIdp) {

CognitoUserService userService = new CognitoUserService()
userService.cognitoIdp = cognitoIdp
Expand All @@ -81,6 +95,8 @@ class Application extends GrailsAutoConfiguration {
userService.jwtProperties = jwtProperties

userService.affiliationsEnabled = grailsApplication.config.getProperty('attributes.affiliations.enabled', Boolean, false)
userService.apiGatewayIdp = gatewayIdp
userService.grailsApplication = grailsApplication

return userService
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import au.org.ala.ws.security.JwtProperties
import au.org.ala.ws.tokens.TokenService
import com.amazonaws.AmazonWebServiceResult
import com.amazonaws.ResponseMetadata
import com.amazonaws.services.apigateway.AmazonApiGateway
import com.amazonaws.services.apigateway.model.CreateApiKeyRequest
import com.amazonaws.services.apigateway.model.CreateUsagePlanKeyRequest
import com.amazonaws.services.apigateway.model.GetApiKeysRequest
import com.amazonaws.services.apigateway.model.GetApiKeysResult
import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProvider
import com.amazonaws.services.cognitoidp.model.AddCustomAttributesRequest
import com.amazonaws.services.cognitoidp.model.AdminAddUserToGroupRequest
Expand All @@ -24,7 +29,8 @@ import com.amazonaws.services.cognitoidp.model.AdminSetUserMFAPreferenceRequest
import com.amazonaws.services.cognitoidp.model.AdminUpdateUserAttributesRequest
import com.amazonaws.services.cognitoidp.model.AssociateSoftwareTokenRequest
import com.amazonaws.services.cognitoidp.model.AttributeType
import com.amazonaws.services.cognitoidp.model.CreateGroupResult
import com.amazonaws.services.cognitoidp.model.CreateUserPoolClientRequest
import com.amazonaws.services.cognitoidp.model.CreateUserPoolClientResult
import com.amazonaws.services.cognitoidp.model.DescribeUserPoolRequest
import com.amazonaws.services.cognitoidp.model.CreateGroupRequest
import com.amazonaws.services.cognitoidp.model.GetGroupRequest
Expand All @@ -44,6 +50,7 @@ import com.amazonaws.services.cognitoidp.model.UserType
import com.nimbusds.oauth2.sdk.token.AccessToken
import com.amazonaws.services.cognitoidp.model.VerifySoftwareTokenRequest
import grails.converters.JSON
import grails.core.GrailsApplication
import grails.web.servlet.mvc.GrailsParameterMap
import groovy.util.logging.Slf4j
import org.apache.commons.lang3.NotImplementedException
Expand All @@ -64,6 +71,8 @@ class CognitoUserService implements IUserService<UserRecord, UserPropertyRecord,
AWSCognitoIdentityProvider cognitoIdp
String poolId
JwtProperties jwtProperties
AmazonApiGateway apiGatewayIdp
GrailsApplication grailsApplication

@Value('${attributes.affiliations.enabled:false}')
boolean affiliationsEnabled = false
Expand Down Expand Up @@ -858,4 +867,78 @@ class CognitoUserService implements IUserService<UserRecord, UserPropertyRecord,
resultStreamer.complete()
}

@Override
Map generateApikey(String usagePlanId) {
if(!usagePlanId){
return [apikeys:null, err: "No usage plan id to generate api key"]
}

CreateApiKeyRequest request = new CreateApiKeyRequest()
request.enabled = true
request.customerId = currentUser.userId
request.name = "API key for user " + currentUser.userId
def response = apiGatewayIdp.createApiKey(request)

if(isSuccessful(response)) {
//add api key to usage plan
CreateUsagePlanKeyRequest usagePlanKeyRequest = new CreateUsagePlanKeyRequest()
usagePlanKeyRequest.keyId = response.id
usagePlanKeyRequest.keyType = "API_KEY"
usagePlanKeyRequest.usagePlanId = usagePlanId
apiGatewayIdp.createUsagePlanKey(usagePlanKeyRequest)

return [apikeys:getApikeys(currentUser.userId), error: null]
}
else{
return [apikeys:null, error: "Could not generate api key"]
}
}

@Override
def getApikeys(String userId) {

GetApiKeysRequest getApiKeysRequest = new GetApiKeysRequest().withCustomerId(userId).withIncludeValues(true)
GetApiKeysResult response = apiGatewayIdp.getApiKeys(getApiKeysRequest)
if(isSuccessful(response)){
return response.items.value
}
else{
return null
}
}

@Override
def generateClient(String userId, List<String> callbackURLs, boolean forGalah){
CreateUserPoolClientRequest request = new CreateUserPoolClientRequest().withUserPoolId(poolId)
request.clientName = "Client for user " + userId
request.allowedOAuthFlows = ["code"]
request.generateSecret = false
request.supportedIdentityProviders = ["COGNITO", "Facebook", "Google", "AAF"] //"SignInWithApple"
request.preventUserExistenceErrors = "ENABLED"
request.explicitAuthFlows = ["ALLOW_REFRESH_TOKEN_AUTH", "ALLOW_CUSTOM_AUTH", "ALLOW_USER_SRP_AUTH", "ALLOW_USER_PASSWORD_AUTH"]
request.allowedOAuthFlowsUserPoolClient = true

def scopes = grailsApplication.config.getProperty('oauth.support.dynamic.client.scopes', List, [])

if(scopes) {
request.allowedOAuthScopes = scopes
}

request.callbackURLs = callbackURLs
if(forGalah) {
request.callbackURLs.addAll(grailsApplication.config.getProperty('oauth.support.dynamic.client.galah.callbackURLs', List, []))
}

CreateUserPoolClientResult response = cognitoIdp.createUserPoolClient(request)

if(isSuccessful(response)){
//update user custom attribute with new clientId
addCustomUserProperty(currentUser, "clientId", response.userPoolClient.clientId)
return [apikeys: response.userPoolClient.clientId, error: null]
}
else{
return [clientId: null, error: "Could not generate client"]
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ import au.org.ala.userdetails.LocationService
import au.org.ala.userdetails.PasswordService
import au.org.ala.web.AuthService
import au.org.ala.ws.service.WebService
import com.amazonaws.auth.AWSCredentials
import com.amazonaws.auth.AWSCredentialsProvider
import com.amazonaws.auth.AWSStaticCredentialsProvider
import com.amazonaws.auth.BasicAWSCredentials
import com.amazonaws.auth.BasicSessionCredentials
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain
import com.amazonaws.services.apigateway.AmazonApiGateway
import com.amazonaws.services.apigateway.AmazonApiGatewayClientBuilder
import grails.boot.GrailsApp
import grails.boot.config.GrailsAutoConfiguration
import grails.core.GrailsApplication
Expand All @@ -45,14 +53,49 @@ class Application extends GrailsAutoConfiguration {
new DataSourceHealthIndicator(dataSource)
}

@Bean
AWSCredentialsProvider awsCredentialsProvider() {

String accessKey = grailsApplication.config.getProperty('apigateway.accessKey')
String secretKey = grailsApplication.config.getProperty('apigateway.secretKey')
String sessionToken = grailsApplication.config.getProperty('apigateway.sessionToken')

AWSCredentialsProvider credentialsProvider
if (accessKey && secretKey) {
AWSCredentials credentials
if (sessionToken) {
credentials = new BasicSessionCredentials(accessKey, secretKey, sessionToken)
} else {
credentials = new BasicAWSCredentials(accessKey, secretKey)
}
credentialsProvider = new AWSStaticCredentialsProvider(credentials)
} else {
credentialsProvider = DefaultAWSCredentialsProviderChain.getInstance()
}
return credentialsProvider
}

@Bean
AmazonApiGateway gatewayIdpClient(AWSCredentialsProvider awsCredentialsProvider) {
def region = grailsApplication.config.getProperty('apigateway.region')

AmazonApiGateway gatewayIdp = AmazonApiGatewayClientBuilder.standard()
.withRegion(region)
.withCredentials(awsCredentialsProvider)
.build()

return gatewayIdp
}

@Bean('userService')
IUserService userService(GrailsApplication grailsApplication,
EmailService emailService,
PasswordService passwordService,
AuthService authService,
LocationService locationService,
MessageSource messageSource,
WebService webService
WebService webService,
AmazonApiGateway gatewayIdp
) {

// grailsApplication.addArtefact(DomainClassArtefactHandler.TYPE, UserRecord)
Expand All @@ -70,6 +113,7 @@ class Application extends GrailsAutoConfiguration {
userService.messageSource = messageSource

userService.affiliationsEnabled = grailsApplication.config.getProperty('attributes.affiliations.enabled', Boolean, false)
userService.apiGatewayIdp = gatewayIdp

return userService
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@ import au.org.ala.userdetails.PasswordService
import au.org.ala.userdetails.ResultStreamer
import au.org.ala.web.AuthService
import au.org.ala.ws.service.WebService
import com.amazonaws.services.apigateway.AmazonApiGateway
import com.amazonaws.services.apigateway.model.CreateApiKeyRequest
import com.amazonaws.services.apigateway.model.CreateUsagePlanKeyRequest
import com.amazonaws.services.apigateway.model.GetApiKeysRequest
import com.amazonaws.services.apigateway.model.GetApiKeysResult
import grails.converters.JSON
import grails.core.GrailsApplication
import grails.plugin.cache.Cacheable
import grails.gorm.transactions.Transactional
import grails.util.Environment
import grails.web.servlet.mvc.GrailsParameterMap
import groovy.util.logging.Slf4j
import org.apache.commons.lang3.NotImplementedException
import org.apache.http.HttpStatus
import org.grails.datastore.mapping.core.Session
import org.grails.orm.hibernate.cfg.GrailsHibernateUtil
Expand All @@ -50,6 +56,7 @@ class GormUserService implements IUserService<User, UserProperty, Role, UserRole
LocationService locationService
MessageSource messageSource
WebService webService
AmazonApiGateway apiGatewayIdp

@Value('${attributes.affiliations.enabled:false}')
boolean affiliationsEnabled = false
Expand Down Expand Up @@ -700,4 +707,48 @@ class GormUserService implements IUserService<User, UserProperty, Role, UserRole
}
return results
}

@Override
Map generateApikey(String usagePlanId) {
if(!usagePlanId){
return [apikeys:null, err: "No usage plan id to generate api key"]
}

CreateApiKeyRequest request = new CreateApiKeyRequest()
request.enabled = true
request.customerId = currentUser.userId
request.name = "API key for user " + currentUser.userId
def response = apiGatewayIdp.createApiKey(request)

if(response.getSdkHttpMetadata().httpStatusCode == 201) {
//add api key to usage plan
CreateUsagePlanKeyRequest usagePlanKeyRequest = new CreateUsagePlanKeyRequest()
usagePlanKeyRequest.keyId = response.id
usagePlanKeyRequest.keyType = "API_KEY"
usagePlanKeyRequest.usagePlanId = usagePlanId
apiGatewayIdp.createUsagePlanKey(usagePlanKeyRequest)

return [apikeys:getApikeys(currentUser.userId), err: null]
}
else{
return [apikeys:null, err: "Could not generate api key"]
}
}

@Override
def getApikeys(String userId) {
GetApiKeysRequest getApiKeysRequest = new GetApiKeysRequest().withCustomerId(userId).withIncludeValues(true)
GetApiKeysResult response = apiGatewayIdp.getApiKeys(getApiKeysRequest)
if(response.getSdkHttpMetadata().httpStatusCode == 200){
return response.items.value
}
else{
return null
}
}

@Override
def generateClient(String userId, List<String> callbackURLs, boolean forGalah){
throw new NotImplementedException()
}
}
2 changes: 2 additions & 0 deletions userdetails-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ dependencies {

testImplementation('com.squareup.retrofit2:retrofit-mock:2.9.0')
testImplementation 'io.github.joke:spock-mockable:2.3.0'

api 'com.amazonaws:aws-java-sdk-api-gateway:1.12.279'
}

compileJava.dependsOn(processResources)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,48 @@ class ProfileController {
}
redirect(controller: 'profile')
}

def myClientAndApikey() {
def user = userService.currentUser
def clientId = user.additionalAttributes.find { it.name == 'clientId' }?.value
render view: "myClientAndApikey", model: [apikeys: String.join(",", userService.getApikeys(user.userId)), clientId: clientId]
}

def generateApikey(String application) {
if(!application) {
render(view: "myClientAndApikey", model:[ errors: ['No application name']])
return
}

String usagePlanId = grailsApplication.config.getProperty("apigateway.${application}.usagePlanId")

if(!usagePlanId) {
render(view: "myClientAndApikey", model:[ errors: ['No usage plan id to generate api key']])
return
}
def response = userService.generateApikey(usagePlanId)
if(response.error) {
render view: "myClientAndApikey", model:[ errors: [response.error]]
return
}
redirect(action: "myClientAndApikey")
}

def generateClient() {

def isForGalah = params.forGalah? true: false
List<String> callbackURLs = params.list('callbackURLs').findAll {it != ""}

if(!isForGalah && callbackURLs.empty){
render(view: "myClientAndApikey", model:[ errors: ["callbackURLs cannot be empty if the client is not for Galah"]])
return
}

def response = userService.generateClient(userService.currentUser.userId, callbackURLs, isForGalah)
if(response.error) {
render(view: "myClientAndApikey", model:[ errors: [response.error]])
return
}
redirect(action: "myClientAndApikey")
}
}
15 changes: 15 additions & 0 deletions userdetails-plugin/grails-app/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,18 @@ user.lastLogin.label=Last login
user.lastUpdated.label=Last updated

reload.config=Reload external config

myprofile.myClientAndApikey=My Client And Apikey
myprofile.myClientAndApikey.desc=View my client and apikey
myclient.desc=To access protected ALA apis, you need a client id to generate an access token.
myclient.callbackURLs=Comma seperated callback URLs of your client (optional)
my.client.id=My Client id :
myprofile.my.apikey=My Apikey
myprofile.my.client=My Client
myprofile.my.client.create=Create My Client
myprofile.my.apikey.desc=For Galah you also need to use the below api key.
myprofile.generate.apikey=Generate Apikey
myprofile.generate.client=Generate Client Id
my.apikey=My API key :
generate.apikey.desc.1=The apikey is used to identify the project/application or site which makes the call to an API. API key is not used for authentication but rather for usage tracking, monitoring, and rate limiting due to the expected high frequency of usage on the endpoints.
generate.apikey.desc.2=The generated apikey can be used when making a call to an ALA API. The apikey should be set as "x-api-key" header in the request.
Loading

0 comments on commit de139bb

Please sign in to comment.