
mn create-app --build=gradle --jdk=8 --lang=groovy --test=junit --features=security-session,data-jdbc,views-thymeleaf,h2 com.sap.sflight
dependencies {
implementation("io.micronaut:micronaut-http-client")
implementation("io.micronaut.data:micronaut-data-jdbc:3.4.2")
implementation("io.micronaut.groovy:micronaut-runtime-groovy")
implementation("io.micronaut.security:micronaut-security-session")
implementation("io.micronaut.sql:micronaut-jdbc-hikari")
implementation("io.micronaut.views:micronaut-views-thymeleaf")
compileOnly("io.micronaut:micronaut-http-validation")
compileOnly("io.micronaut.data:micronaut-data-processor:3.4.2")
compileOnly("io.micronaut.security:micronaut-security-annotations")
runtimeOnly("ch.qos.logback:logback-classic")
runtimeOnly("com.h2database:h2")
implementation("io.micronaut.rxjava3:micronaut-rxjava3")
implementation("io.micronaut.rxjava3:micronaut-rxjava3-http-client")
implementation("io.micronaut:micronaut-validation")
implementation('com.sap.cloud.db.jdbc:ngdbc:2.13.9') //sap jdbc driver.
implementation('io.pivotal.cfenv:java-cfenv-boot:2.4.0') //to access the VCAP variables.
implementation('com.sap.cloud.security:java-security:2.13.0') //to read the claims of the token.
implementation('com.sap.cloud.security:sapjwt:1.5.27.5') //to validate the JWT token.
}
Authentication flow
package com.sap
import io.micronaut.data.annotation.MappedEntity
import jakarta.persistence.Column
import jakarta.persistence.Id
@MappedEntity('SFLIGHT')
class SFlightEntity {
@Id
@Column(name = 'ID')
Integer flightId
@Column(name = 'FLIGHTNAME')
String flightName
@Column(name = 'FLIGHTFROM')
String flightFrom
@Column(name = 'FLIGHTTO')
String flightTo
@Column(name = 'FLIGHTDATE')
Date flightDate
}
package com.sap
import io.micronaut.data.jdbc.annotation.JdbcRepository
import io.micronaut.data.model.query.builder.sql.Dialect
import io.micronaut.data.repository.CrudRepository
@JdbcRepository(dialect = Dialect.ORACLE)
interface SFlightRepository extends CrudRepository<SFlightEntity, Integer> {
//no need to define additional methods. The methods of standard API CrudRepository is sufficient for requirement.
}
package com.sap
import io.micronaut.core.annotation.Nullable
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.PathVariable
import io.micronaut.http.annotation.Post
import io.micronaut.security.annotation.Secured
import io.micronaut.security.rules.SecurityRule
import io.micronaut.views.View
import jakarta.inject.Inject
import java.security.Principal
@Controller('/home')
class SFlightController {
SFlightRepository sFlightRepository
@Inject
SFlightController(SFlightRepository sFlightRepository) {
this.sFlightRepository = sFlightRepository
//bootstrap some data in the sflight table, if the table is not having any records.
if (this.sFlightRepository.findAll().isEmpty()) {
this.sFlightRepository.save(new SFlightEntity(flightId: 1,
flightName: 'Air France',
flightFrom: 'Paris',
flightTo: 'Amsterdam',
flightDate: new Date()))
this.sFlightRepository.save(new SFlightEntity(flightId: 2,
flightFrom: 'New Delhi',
flightName: 'Air India',
flightTo: 'Frankfurt',
flightDate: new Date()))
this.sFlightRepository.save(new SFlightEntity(flightId: 3,
flightName: 'Delta',
flightFrom: 'Los Angeles',
flightTo: 'Heathrow',
flightDate: new Date()))
}
}
@Get('/')
@Secured([SecurityRule.IS_ANONYMOUS, 'ADMIN'])
@View('home')
//everyone can read the list of the flight.
Map<String, Object> getAll(@Nullable Principal principal) {
return ['flightList': this.sFlightRepository.findAll(), 'user': principal.getName()]
}
@Post(value = '/post', consumes = MediaType.APPLICATION_FORM_URLENCODED)
@Secured('ADMIN')
@View('action')
//only admin can perform the operations.
Map create(@Body SFlightEntity sFlightEntity) {
try {
this.sFlightRepository.save(sFlightEntity)
return ['message': 'Entry created', 'sFlight': sFlightEntity]
}
catch (Exception exception) {
return ['message': exception.getMessage(), 'sFlight': sFlightEntity]
}
}
@Post(value = '/put/{id}', consumes = MediaType.APPLICATION_FORM_URLENCODED)
@Secured('ADMIN')
@View('action')
//only admin can perform the operations.
Map update(@Body SFlightEntity sFlightEntity, @PathVariable('id') String id) {
try {
this.sFlightRepository.update(sFlightEntity)
return ['message': "${id} Entry updated", 'sFlight': sFlightEntity]
}
catch (Exception exception) {
return ['message': exception.getMessage(), 'sFlight': sFlightEntity]
}
}
@Post(value = '/delete/{id}', consumes = MediaType.APPLICATION_FORM_URLENCODED)
@Secured('ADMIN')
@View('action')
//only admin can perform the operations.
Map delete(@Body SFlightEntity sFlightEntity, @PathVariable('id') String id) {
try {
this.sFlightRepository.deleteById(sFlightEntity.getFlightId())
return ['message': "${id} Entry deleted", 'sFlight': sFlightEntity]
}
catch (Exception exception) {
return ['message': exception.getMessage(), 'sFlight': sFlightEntity]
}
}
@Get('/operation/{+name}')
@Secured('ADMIN')
@View('action')
//only admin can perform the operations.
Map<String, Object> operation(@Nullable @PathVariable('name') String name) {
String operationName = (name.split('/')[0] as String).toUpperCase() //operation name
Integer id
SFlightEntity sFlightEntity
switch (operationName) {
case 'POST':
return ['sFlight': new SFlightEntity(), 'operation': name]
case 'PUT':
case 'DELETE':
id = name.split('/')[1] as Integer
sFlightEntity = this.sFlightRepository.findById(id).get()
return ['sFlight': sFlightEntity, 'operation': name]
}
}
}
package com.sap
import io.micronaut.context.ApplicationContext
import io.micronaut.context.env.PropertySource
import io.micronaut.http.HttpRequest
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.server.util.HttpHostResolver
import io.micronaut.security.annotation.Secured
import io.micronaut.security.rules.SecurityRule
import io.micronaut.views.View
import io.pivotal.cfenv.core.CfEnv
import jakarta.inject.Inject
@Controller('/')
class LandingController {
HttpHostResolver hostResolver
ApplicationContext applicationContext
@Inject
LandingController(HttpHostResolver hostResolver, ApplicationContext applicationContext) {
this.hostResolver = hostResolver
}
@Get('/')
@Secured(SecurityRule.IS_ANONYMOUS)
@View('landing')
Map landingZone(HttpRequest httpRequest) {
String uri
if (this.hostResolver.resolve(httpRequest).toUpperCase() == 'HTTP://LOCALHOST:8080') {
return ['token': 'DUMMY', 'uri': this.hostResolver.resolve(httpRequest)]
} else {
uri = "https://" + new CfEnv().getApp().getApplicationUris()[0] //actual URL when deployed in the cloud.
return ['token': httpRequest.getHeaders().getAuthorization().get(), 'uri': uri]
}
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>App</title>
<link rel="icon" type="image/png" sizes="16x16" href="static/favicon-16x16.png">
</head>
<body onload="document.forms[0].submit()" style="background:blue; color:white;">
<p>Please wait...</p>
<form method="post" th:attr="action=${uri}+'/login'">
<input type="hidden" name="username" id="username" th:value="${token}"/>
<input type="hidden" name="password" id="password" th:value="${token}"/>
</form>
</body>
</html>
package com.sap
import com.fasterxml.jackson.databind.ObjectMapper
import com.sap.cloud.security.jwt.JwtValidation
import com.sap.cloud.security.token.Token
import com.sap.cloud.security.token.TokenClaims
import io.micronaut.core.annotation.Nullable
import io.micronaut.http.HttpRequest
import io.micronaut.http.server.util.HttpHostResolver
import io.micronaut.security.authentication.AuthenticationProvider
import io.micronaut.security.authentication.AuthenticationRequest
import io.micronaut.security.authentication.AuthenticationResponse
import io.pivotal.cfenv.core.CfEnv
import io.reactivex.rxjava3.annotations.NonNull
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.FlowableEmitter
import io.reactivex.rxjava3.core.FlowableOnSubscribe
import io.reactivex.rxjava3.schedulers.Schedulers
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
@Singleton
class Authenticator implements AuthenticationProvider {
HttpHostResolver hostResolver
@Inject
LandingController(HttpHostResolver hostResolver) {
this.hostResolver = hostResolver
}
Publisher<AuthenticationResponse> authenticate(@Nullable HttpRequest<?> httpRequest,
AuthenticationRequest<?, ?> authenticationRequest) {
return Flowable<AuthenticationResponse>.create(new FlowableOnSubscribe<AuthenticationResponse>() {
@Override
void subscribe(@NonNull FlowableEmitter<AuthenticationResponse> emitter) throws Throwable {
try {
//for the local testing take the dummy token and for the actual server use the JWT.
if (hostResolver.resolve(httpRequest).toUpperCase() == 'HTTP://LOCALHOST:8080') {
emitter.onNext(AuthenticationResponse.success(authenticationRequest.getIdentity() as String, ['ADMIN']))
emitter.onComplete()
} else {
//this means that the app is actually deployed in BTP.
String token = authenticationRequest.getIdentity()
Token tokenRead = Token.create(token) //read the token.
Map role = new ObjectMapper().readValue(tokenRead.getClaimAsJsonObject('xs.system.attributes')
.asJsonString(), Map.class) //read the roles.
String email = tokenRead.getClaimAsString(TokenClaims.EMAIL) //read the email.
String key = new CfEnv().findServiceByLabel('xsuaa')
.getCredentials()
.getMap()
.get('verificationkey') //Get the public key certificate.
key = key.replaceAll('-----BEGIN PUBLIC KEY-----', '')
.replaceAll('-----END PUBLIC KEY-----', '')
.replaceAll('\\n', '')
String validationResult = new JwtValidation().checkJwToken(token.substring(7), key)
emitter.onNext(AuthenticationResponse.success(email, role.get('xs.rolecollections') as Collection<String>))
emitter.onComplete()
}
}
catch (Exception exception) {
emitter.onNext(AuthenticationResponse.failure(exception.getMessage()))
emitter.onComplete()
}
}
}, BackpressureStrategy.ERROR).subscribeOn(Schedulers.io())
}
}
{
"xsappname": "sflight-15ee76b5trial",
"tenant-mode": "shared",
"scopes": [
{
"name": "$XSAPPNAME.Admin",
"description": "admin"
}
],
"role-templates": [
{
"name": "ADMIN",
"description": "SFLIGHT admin role",
"scope-references" : [
"$XSAPPNAME.Admin"
]
}
]
}
cf create-service xsuaa application my-xsuaa -c xs-security.json
---
applications:
- name: sflight
timeout: 600
memory: 1G
instances: 1
path: sflight-0.1-all.jar
buildpack: java_buildpack
cf push sflight -f "manifest.yml"
---
applications:
- name: approuter
routes:
- route: approuter-15ee76b5trial.cfapps.us10.hana.ondemand.com
path: approuter
memory: 128M
buildpacks:
- nodejs_buildpack
env:
TENANT_HOST_PATTERN: 'approuter-(.*).cfapps.us10.hana.ondemand.com'
destinations: '[{"name":"app-destination", "url" :"https://sflight.cfapps.us10.hana.ondemand.com", "forwardAuthToken": true}]'
services:
- my-xsuaa
cf push
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
9 | |
7 | |
7 | |
6 | |
5 | |
5 | |
4 | |
4 | |
4 | |
4 |