Skip to content

Commit b56aa21

Browse files
committed
Add apikey and client generation feature #156
1 parent 8b36f6e commit b56aa21

File tree

11 files changed

+386
-4
lines changed

11 files changed

+386
-4
lines changed

userdetails-cognito/grails-app/conf/application.yml

+3
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,6 @@ account:
198198
MFAenabled: true
199199
authorised-systems:
200200
edit-enabled: false
201+
oauth.support.dynamic.client.registration: true
202+
oauth.support.dynamic.client.scopes: ["email", "openid", "profile", "ala/attrs" , "ala/roles"]
203+
tokenApp.tokenGeneration.url: https://tokens-cognito-support.dev.ala.org.au?step=generation

userdetails-cognito/grails-app/init/au/org/ala/userdetails/cognito/Application.groovy

+18-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ import au.org.ala.web.OidcClientProperties
2020
import au.org.ala.ws.security.JwtProperties
2121
import au.org.ala.ws.tokens.TokenService
2222
import com.amazonaws.auth.*
23+
import com.amazonaws.services.apigateway.AmazonApiGateway
24+
import com.amazonaws.services.apigateway.AmazonApiGatewayClientBuilder
2325
import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProvider
24-
import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProviderClient
2526
import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProviderClientBuilder
2627
import grails.boot.GrailsApp
2728
import grails.boot.config.GrailsAutoConfiguration
@@ -69,8 +70,21 @@ class Application extends GrailsAutoConfiguration {
6970
return cognitoIdp
7071
}
7172

73+
@Bean
74+
AmazonApiGateway gatewayIdpClient(AWSCredentialsProvider awsCredentialsProvider) {
75+
def region = grailsApplication.config.getProperty('cognito.region')
76+
77+
AmazonApiGateway gatewayIdp = AmazonApiGatewayClientBuilder.standard()
78+
.withRegion(region)
79+
.withCredentials(awsCredentialsProvider)
80+
.build()
81+
82+
return gatewayIdp
83+
}
84+
7285
@Bean('userService')
73-
IUserService userService(TokenService tokenService, EmailService emailService, AWSCognitoIdentityProvider cognitoIdp, JwtProperties jwtProperties) {
86+
IUserService userService(TokenService tokenService, EmailService emailService, AWSCognitoIdentityProvider cognitoIdp, JwtProperties jwtProperties,
87+
AmazonApiGateway gatewayIdp) {
7488

7589
CognitoUserService userService = new CognitoUserService()
7690
userService.cognitoIdp = cognitoIdp
@@ -81,6 +95,8 @@ class Application extends GrailsAutoConfiguration {
8195
userService.jwtProperties = jwtProperties
8296

8397
userService.affiliationsEnabled = grailsApplication.config.getProperty('attributes.affiliations.enabled', Boolean, false)
98+
userService.apiGatewayIdp = gatewayIdp
99+
userService.grailsApplication = grailsApplication
84100

85101
return userService
86102
}

userdetails-cognito/src/main/groovy/au/org/ala/userdetails/CognitoUserService.groovy

+84-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import au.org.ala.ws.security.JwtProperties
99
import au.org.ala.ws.tokens.TokenService
1010
import com.amazonaws.AmazonWebServiceResult
1111
import com.amazonaws.ResponseMetadata
12+
import com.amazonaws.services.apigateway.AmazonApiGateway
13+
import com.amazonaws.services.apigateway.model.CreateApiKeyRequest
14+
import com.amazonaws.services.apigateway.model.CreateUsagePlanKeyRequest
15+
import com.amazonaws.services.apigateway.model.GetApiKeysRequest
16+
import com.amazonaws.services.apigateway.model.GetApiKeysResult
1217
import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProvider
1318
import com.amazonaws.services.cognitoidp.model.AddCustomAttributesRequest
1419
import com.amazonaws.services.cognitoidp.model.AdminAddUserToGroupRequest
@@ -24,7 +29,8 @@ import com.amazonaws.services.cognitoidp.model.AdminSetUserMFAPreferenceRequest
2429
import com.amazonaws.services.cognitoidp.model.AdminUpdateUserAttributesRequest
2530
import com.amazonaws.services.cognitoidp.model.AssociateSoftwareTokenRequest
2631
import com.amazonaws.services.cognitoidp.model.AttributeType
27-
import com.amazonaws.services.cognitoidp.model.CreateGroupResult
32+
import com.amazonaws.services.cognitoidp.model.CreateUserPoolClientRequest
33+
import com.amazonaws.services.cognitoidp.model.CreateUserPoolClientResult
2834
import com.amazonaws.services.cognitoidp.model.DescribeUserPoolRequest
2935
import com.amazonaws.services.cognitoidp.model.CreateGroupRequest
3036
import com.amazonaws.services.cognitoidp.model.GetGroupRequest
@@ -44,6 +50,7 @@ import com.amazonaws.services.cognitoidp.model.UserType
4450
import com.nimbusds.oauth2.sdk.token.AccessToken
4551
import com.amazonaws.services.cognitoidp.model.VerifySoftwareTokenRequest
4652
import grails.converters.JSON
53+
import grails.core.GrailsApplication
4754
import grails.web.servlet.mvc.GrailsParameterMap
4855
import groovy.util.logging.Slf4j
4956
import org.apache.commons.lang3.NotImplementedException
@@ -64,6 +71,8 @@ class CognitoUserService implements IUserService<UserRecord, UserPropertyRecord,
6471
AWSCognitoIdentityProvider cognitoIdp
6572
String poolId
6673
JwtProperties jwtProperties
74+
AmazonApiGateway apiGatewayIdp
75+
GrailsApplication grailsApplication
6776

6877
@Value('${attributes.affiliations.enabled:false}')
6978
boolean affiliationsEnabled = false
@@ -880,4 +889,78 @@ class CognitoUserService implements IUserService<UserRecord, UserPropertyRecord,
880889
resultStreamer.complete()
881890
}
882891

892+
@Override
893+
Map generateApikey(String usagePlanId) {
894+
if(!usagePlanId){
895+
return [apikeys:null, err: "No usage plan id to generate api key"]
896+
}
897+
898+
CreateApiKeyRequest request = new CreateApiKeyRequest()
899+
request.enabled = true
900+
request.customerId = currentUser.userId
901+
request.name = "API key for user " + currentUser.userId
902+
def response = apiGatewayIdp.createApiKey(request)
903+
904+
if(isSuccessful(response)) {
905+
//add api key to usage plan
906+
CreateUsagePlanKeyRequest usagePlanKeyRequest = new CreateUsagePlanKeyRequest()
907+
usagePlanKeyRequest.keyId = response.id
908+
usagePlanKeyRequest.keyType = "API_KEY"
909+
usagePlanKeyRequest.usagePlanId = usagePlanId
910+
apiGatewayIdp.createUsagePlanKey(usagePlanKeyRequest)
911+
912+
return [apikeys:getApikeys(currentUser.userId), error: null]
913+
}
914+
else{
915+
return [apikeys:null, error: "Could not generate api key"]
916+
}
917+
}
918+
919+
@Override
920+
def getApikeys(String userId) {
921+
922+
GetApiKeysRequest getApiKeysRequest = new GetApiKeysRequest().withCustomerId(userId).withIncludeValues(true)
923+
GetApiKeysResult response = apiGatewayIdp.getApiKeys(getApiKeysRequest)
924+
if(isSuccessful(response)){
925+
return response.items.value
926+
}
927+
else{
928+
return null
929+
}
930+
}
931+
932+
@Override
933+
def generateClient(String userId, List<String> callbackURLs, boolean forGalah){
934+
CreateUserPoolClientRequest request = new CreateUserPoolClientRequest().withUserPoolId(poolId)
935+
request.clientName = "Client for user " + userId
936+
request.allowedOAuthFlows = ["code"]
937+
request.generateSecret = false
938+
request.supportedIdentityProviders = ["COGNITO", "Facebook", "Google", "AAF"] //"SignInWithApple"
939+
request.preventUserExistenceErrors = "ENABLED"
940+
request.explicitAuthFlows = ["ALLOW_REFRESH_TOKEN_AUTH", "ALLOW_CUSTOM_AUTH", "ALLOW_USER_SRP_AUTH", "ALLOW_USER_PASSWORD_AUTH"]
941+
request.allowedOAuthFlowsUserPoolClient = true
942+
943+
def scopes = grailsApplication.config.getProperty('oauth.support.dynamic.client.scopes', List, [])
944+
945+
if(scopes) {
946+
request.allowedOAuthScopes = scopes
947+
}
948+
949+
request.callbackURLs = callbackURLs
950+
if(forGalah) {
951+
request.callbackURLs.addAll(grailsApplication.config.getProperty('oauth.support.dynamic.client.galah.callbackURLs', List, []))
952+
}
953+
954+
CreateUserPoolClientResult response = cognitoIdp.createUserPoolClient(request)
955+
956+
if(isSuccessful(response)){
957+
//update user custom attribute with new clientId
958+
addCustomUserProperty(currentUser, "clientId", response.userPoolClient.clientId)
959+
return [apikeys: response.userPoolClient.clientId, error: null]
960+
}
961+
else{
962+
return [clientId: null, error: "Could not generate client"]
963+
}
964+
}
965+
883966
}

userdetails-gorm/grails-app/init/au/org/ala/userdetails/gorm/Application.groovy

+45-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ import au.org.ala.userdetails.LocationService
2323
import au.org.ala.userdetails.PasswordService
2424
import au.org.ala.web.AuthService
2525
import au.org.ala.ws.service.WebService
26+
import com.amazonaws.auth.AWSCredentials
27+
import com.amazonaws.auth.AWSCredentialsProvider
28+
import com.amazonaws.auth.AWSStaticCredentialsProvider
29+
import com.amazonaws.auth.BasicAWSCredentials
30+
import com.amazonaws.auth.BasicSessionCredentials
31+
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain
32+
import com.amazonaws.services.apigateway.AmazonApiGateway
33+
import com.amazonaws.services.apigateway.AmazonApiGatewayClientBuilder
2634
import grails.boot.GrailsApp
2735
import grails.boot.config.GrailsAutoConfiguration
2836
import grails.core.GrailsApplication
@@ -45,14 +53,49 @@ class Application extends GrailsAutoConfiguration {
4553
new DataSourceHealthIndicator(dataSource)
4654
}
4755

56+
@Bean
57+
AWSCredentialsProvider awsCredentialsProvider() {
58+
59+
String accessKey = grailsApplication.config.getProperty('apigateway.accessKey')
60+
String secretKey = grailsApplication.config.getProperty('apigateway.secretKey')
61+
String sessionToken = grailsApplication.config.getProperty('apigateway.sessionToken')
62+
63+
AWSCredentialsProvider credentialsProvider
64+
if (accessKey && secretKey) {
65+
AWSCredentials credentials
66+
if (sessionToken) {
67+
credentials = new BasicSessionCredentials(accessKey, secretKey, sessionToken)
68+
} else {
69+
credentials = new BasicAWSCredentials(accessKey, secretKey)
70+
}
71+
credentialsProvider = new AWSStaticCredentialsProvider(credentials)
72+
} else {
73+
credentialsProvider = DefaultAWSCredentialsProviderChain.getInstance()
74+
}
75+
return credentialsProvider
76+
}
77+
78+
@Bean
79+
AmazonApiGateway gatewayIdpClient(AWSCredentialsProvider awsCredentialsProvider) {
80+
def region = grailsApplication.config.getProperty('apigateway.region')
81+
82+
AmazonApiGateway gatewayIdp = AmazonApiGatewayClientBuilder.standard()
83+
.withRegion(region)
84+
.withCredentials(awsCredentialsProvider)
85+
.build()
86+
87+
return gatewayIdp
88+
}
89+
4890
@Bean('userService')
4991
IUserService userService(GrailsApplication grailsApplication,
5092
EmailService emailService,
5193
PasswordService passwordService,
5294
AuthService authService,
5395
LocationService locationService,
5496
MessageSource messageSource,
55-
WebService webService
97+
WebService webService,
98+
AmazonApiGateway gatewayIdp
5699
) {
57100

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

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

74118
return userService
75119
}

userdetails-gorm/src/main/groovy/au/org/ala/userdetails/gorm/GormUserService.groovy

+51
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,19 @@ import au.org.ala.userdetails.PasswordService
2626
import au.org.ala.userdetails.ResultStreamer
2727
import au.org.ala.web.AuthService
2828
import au.org.ala.ws.service.WebService
29+
import com.amazonaws.services.apigateway.AmazonApiGateway
30+
import com.amazonaws.services.apigateway.model.CreateApiKeyRequest
31+
import com.amazonaws.services.apigateway.model.CreateUsagePlanKeyRequest
32+
import com.amazonaws.services.apigateway.model.GetApiKeysRequest
33+
import com.amazonaws.services.apigateway.model.GetApiKeysResult
2934
import grails.converters.JSON
3035
import grails.core.GrailsApplication
3136
import grails.plugin.cache.Cacheable
3237
import grails.gorm.transactions.Transactional
3338
import grails.util.Environment
3439
import grails.web.servlet.mvc.GrailsParameterMap
3540
import groovy.util.logging.Slf4j
41+
import org.apache.commons.lang3.NotImplementedException
3642
import org.apache.http.HttpStatus
3743
import org.grails.datastore.mapping.core.Session
3844
import org.grails.orm.hibernate.cfg.GrailsHibernateUtil
@@ -51,6 +57,7 @@ class GormUserService implements IUserService<User, UserProperty, Role, UserRole
5157
LocationService locationService
5258
MessageSource messageSource
5359
WebService webService
60+
AmazonApiGateway apiGatewayIdp
5461

5562
@Value('${attributes.affiliations.enabled:false}')
5663
boolean affiliationsEnabled = false
@@ -733,4 +740,48 @@ class GormUserService implements IUserService<User, UserProperty, Role, UserRole
733740
}
734741
return results
735742
}
743+
744+
@Override
745+
Map generateApikey(String usagePlanId) {
746+
if(!usagePlanId){
747+
return [apikeys:null, err: "No usage plan id to generate api key"]
748+
}
749+
750+
CreateApiKeyRequest request = new CreateApiKeyRequest()
751+
request.enabled = true
752+
request.customerId = currentUser.userId
753+
request.name = "API key for user " + currentUser.userId
754+
def response = apiGatewayIdp.createApiKey(request)
755+
756+
if(response.getSdkHttpMetadata().httpStatusCode == 201) {
757+
//add api key to usage plan
758+
CreateUsagePlanKeyRequest usagePlanKeyRequest = new CreateUsagePlanKeyRequest()
759+
usagePlanKeyRequest.keyId = response.id
760+
usagePlanKeyRequest.keyType = "API_KEY"
761+
usagePlanKeyRequest.usagePlanId = usagePlanId
762+
apiGatewayIdp.createUsagePlanKey(usagePlanKeyRequest)
763+
764+
return [apikeys:getApikeys(currentUser.userId), err: null]
765+
}
766+
else{
767+
return [apikeys:null, err: "Could not generate api key"]
768+
}
769+
}
770+
771+
@Override
772+
def getApikeys(String userId) {
773+
GetApiKeysRequest getApiKeysRequest = new GetApiKeysRequest().withCustomerId(userId).withIncludeValues(true)
774+
GetApiKeysResult response = apiGatewayIdp.getApiKeys(getApiKeysRequest)
775+
if(response.getSdkHttpMetadata().httpStatusCode == 200){
776+
return response.items.value
777+
}
778+
else{
779+
return null
780+
}
781+
}
782+
783+
@Override
784+
def generateClient(String userId, List<String> callbackURLs, boolean forGalah){
785+
throw new NotImplementedException()
786+
}
736787
}

userdetails-plugin/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ dependencies {
172172

173173
testImplementation('com.squareup.retrofit2:retrofit-mock:2.9.0')
174174
testImplementation 'io.github.joke:spock-mockable:2.3.0'
175+
176+
api 'com.amazonaws:aws-java-sdk-api-gateway:1.12.279'
175177
}
176178

177179
compileJava.dependsOn(processResources)

userdetails-plugin/grails-app/controllers/au/org/ala/userdetails/ProfileController.groovy

+44
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,48 @@ class ProfileController {
182182
}
183183
redirect(controller: 'profile')
184184
}
185+
186+
def myClientAndApikey() {
187+
def user = userService.currentUser
188+
def clientId = user.additionalAttributes.find { it.name == 'clientId' }?.value
189+
render view: "myClientAndApikey", model: [apikeys: String.join(",", userService.getApikeys(user.userId)), clientId: clientId]
190+
}
191+
192+
def generateApikey(String application) {
193+
if(!application) {
194+
render(view: "myClientAndApikey", model:[ errors: ['No application name']])
195+
return
196+
}
197+
198+
String usagePlanId = grailsApplication.config.getProperty("apigateway.${application}.usagePlanId")
199+
200+
if(!usagePlanId) {
201+
render(view: "myClientAndApikey", model:[ errors: ['No usage plan id to generate api key']])
202+
return
203+
}
204+
def response = userService.generateApikey(usagePlanId)
205+
if(response.error) {
206+
render view: "myClientAndApikey", model:[ errors: [response.error]]
207+
return
208+
}
209+
redirect(action: "myClientAndApikey")
210+
}
211+
212+
def generateClient() {
213+
214+
def isForGalah = params.forGalah? true: false
215+
List<String> callbackURLs = params.list('callbackURLs').findAll {it != ""}
216+
217+
if(!isForGalah && callbackURLs.empty){
218+
render(view: "myClientAndApikey", model:[ errors: ["callbackURLs cannot be empty if the client is not for Galah"]])
219+
return
220+
}
221+
222+
def response = userService.generateClient(userService.currentUser.userId, callbackURLs, isForGalah)
223+
if(response.error) {
224+
render(view: "myClientAndApikey", model:[ errors: [response.error]])
225+
return
226+
}
227+
redirect(action: "myClientAndApikey")
228+
}
185229
}

userdetails-plugin/grails-app/i18n/messages.properties

+15
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,18 @@ user.lastLogin.label=Last login
319319
user.lastUpdated.label=Last updated
320320

321321
reload.config=Reload external config
322+
323+
myprofile.myClientAndApikey=My Client And Apikey
324+
myprofile.myClientAndApikey.desc=View my client and apikey
325+
myclient.desc=To access protected ALA apis, you need a client id to generate an access token.
326+
myclient.callbackURLs=Comma seperated callback URLs of your client (optional)
327+
my.client.id=My Client id :
328+
myprofile.my.apikey=My Apikey
329+
myprofile.my.client=My Client
330+
myprofile.my.client.create=Create My Client
331+
myprofile.my.apikey.desc=For Galah you also need to use the below api key.
332+
myprofile.generate.apikey=Generate Apikey
333+
myprofile.generate.client=Generate Client Id
334+
my.apikey=My API key :
335+
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.
336+
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.

0 commit comments

Comments
 (0)