diff --git a/userdetails-cognito/grails-app/conf/application.yml b/userdetails-cognito/grails-app/conf/application.yml index d8d015e2..e8461071 100644 --- a/userdetails-cognito/grails-app/conf/application.yml +++ b/userdetails-cognito/grails-app/conf/application.yml @@ -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 \ No newline at end of file diff --git a/userdetails-cognito/grails-app/init/au/org/ala/userdetails/cognito/Application.groovy b/userdetails-cognito/grails-app/init/au/org/ala/userdetails/cognito/Application.groovy index 779cfe35..b360d730 100644 --- a/userdetails-cognito/grails-app/init/au/org/ala/userdetails/cognito/Application.groovy +++ b/userdetails-cognito/grails-app/init/au/org/ala/userdetails/cognito/Application.groovy @@ -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 @@ -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 @@ -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 } diff --git a/userdetails-cognito/src/main/groovy/au/org/ala/userdetails/CognitoUserService.groovy b/userdetails-cognito/src/main/groovy/au/org/ala/userdetails/CognitoUserService.groovy index 885f9b40..0cc30843 100644 --- a/userdetails-cognito/src/main/groovy/au/org/ala/userdetails/CognitoUserService.groovy +++ b/userdetails-cognito/src/main/groovy/au/org/ala/userdetails/CognitoUserService.groovy @@ -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 @@ -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 @@ -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 @@ -64,6 +71,8 @@ class CognitoUserService implements IUserService 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"] + } + } + } diff --git a/userdetails-gorm/grails-app/init/au/org/ala/userdetails/gorm/Application.groovy b/userdetails-gorm/grails-app/init/au/org/ala/userdetails/gorm/Application.groovy index 34b7f05d..97906e8a 100644 --- a/userdetails-gorm/grails-app/init/au/org/ala/userdetails/gorm/Application.groovy +++ b/userdetails-gorm/grails-app/init/au/org/ala/userdetails/gorm/Application.groovy @@ -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 @@ -45,6 +53,40 @@ 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, @@ -52,7 +94,8 @@ class Application extends GrailsAutoConfiguration { AuthService authService, LocationService locationService, MessageSource messageSource, - WebService webService + WebService webService, + AmazonApiGateway gatewayIdp ) { // grailsApplication.addArtefact(DomainClassArtefactHandler.TYPE, UserRecord) @@ -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 } diff --git a/userdetails-gorm/src/main/groovy/au/org/ala/userdetails/gorm/GormUserService.groovy b/userdetails-gorm/src/main/groovy/au/org/ala/userdetails/gorm/GormUserService.groovy index 4177512a..bd6c48ff 100644 --- a/userdetails-gorm/src/main/groovy/au/org/ala/userdetails/gorm/GormUserService.groovy +++ b/userdetails-gorm/src/main/groovy/au/org/ala/userdetails/gorm/GormUserService.groovy @@ -25,6 +25,11 @@ 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 @@ -32,6 +37,7 @@ 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 @@ -50,6 +56,7 @@ class GormUserService implements IUserService callbackURLs, boolean forGalah){ + throw new NotImplementedException() + } } diff --git a/userdetails-plugin/build.gradle b/userdetails-plugin/build.gradle index 1f8e0036..918f20d9 100644 --- a/userdetails-plugin/build.gradle +++ b/userdetails-plugin/build.gradle @@ -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) diff --git a/userdetails-plugin/grails-app/controllers/au/org/ala/userdetails/ProfileController.groovy b/userdetails-plugin/grails-app/controllers/au/org/ala/userdetails/ProfileController.groovy index e2432b15..206ab0dc 100644 --- a/userdetails-plugin/grails-app/controllers/au/org/ala/userdetails/ProfileController.groovy +++ b/userdetails-plugin/grails-app/controllers/au/org/ala/userdetails/ProfileController.groovy @@ -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 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") + } } diff --git a/userdetails-plugin/grails-app/i18n/messages.properties b/userdetails-plugin/grails-app/i18n/messages.properties index 52f61413..f99b161d 100644 --- a/userdetails-plugin/grails-app/i18n/messages.properties +++ b/userdetails-plugin/grails-app/i18n/messages.properties @@ -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. \ No newline at end of file diff --git a/userdetails-plugin/grails-app/views/profile/myClientAndApikey.gsp b/userdetails-plugin/grails-app/views/profile/myClientAndApikey.gsp new file mode 100644 index 00000000..06b2dced --- /dev/null +++ b/userdetails-plugin/grails-app/views/profile/myClientAndApikey.gsp @@ -0,0 +1,90 @@ +%{-- + - Copyright (C) 2022 Atlas of Living Australia + - All Rights Reserved. + - + - The contents of this file are subject to the Mozilla Public + - License Version 1.1 (the "License"); you may not use this file + - except in compliance with the License. You may obtain a copy of + - the License at http://www.mozilla.org/MPL/ + - + - Software distributed under the License is distributed on an "AS + - IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + - implied. See the License for the specific language governing + - rights and limitations under the License. + --}% + + + + + + + <g:message code="myprofile.myClientAndApikey" /> + + + + +
+

+ +
+ +

+
+
+
+ +
+
+

+ +

+ + ${clientId} + + + +
+

+
+ + +
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+

+

+ +
+

+

+
+ + ${apikeys} + + + + +
+
+
+
+
+ + \ No newline at end of file diff --git a/userdetails-plugin/grails-app/views/profile/myprofile.gsp b/userdetails-plugin/grails-app/views/profile/myprofile.gsp index e386f9a5..0f2c4c40 100644 --- a/userdetails-plugin/grails-app/views/profile/myprofile.gsp +++ b/userdetails-plugin/grails-app/views/profile/myprofile.gsp @@ -75,6 +75,19 @@ +
+
+ +
+
+

+ + + +

+

+
+
diff --git a/userdetails-plugin/src/main/groovy/au/org/ala/userdetails/IUserService.groovy b/userdetails-plugin/src/main/groovy/au/org/ala/userdetails/IUserService.groovy index 135127c5..0d9d4ff7 100644 --- a/userdetails-plugin/src/main/groovy/au/org/ala/userdetails/IUserService.groovy +++ b/userdetails-plugin/src/main/groovy/au/org/ala/userdetails/IUserService.groovy @@ -226,4 +226,25 @@ interface IUserService, P extends IUserP void removeUserProperty(U userRecord, ArrayList attributes) List

searchProperty(U userRecord, String attribute) + + /*** + * This method is used to generate an api key for a given aws apigateway usage plan + * @param usagePlanId + * @return + */ + Map generateApikey(String usagePlanId) + + /*** + * This method is used to get registered api keys of a user + * @param userId + * @return + */ + def getApikeys(String userId) + + /*** + * This method is used to generate an oauth client + * @param userId + * @return + */ + def generateClient(String userId, List callbackURLs, boolean forGalah) } \ No newline at end of file