Skip to content
This repository was archived by the owner on Nov 11, 2024. It is now read-only.

Commit

Permalink
Add auth routes to authentication servlet to communicate with Dex (#490)
Browse files Browse the repository at this point in the history
* Add /auth and /auth/callback GET requests to OpenAPI docs

Signed-off-by: Eamonn Mansour <[email protected]>

* Bump api.common and authentication bundles to 0.31.0

Signed-off-by: Eamonn Mansour <[email protected]>

* Allow root routes to match without trailing / and update mock servlet implementations

Signed-off-by: Eamonn Mansour <[email protected]>

* Add GET method for /auth, change auth servlet to use routes

Signed-off-by: Eamonn Mansour <[email protected]>

* Add /auth/callback route and unit tests

Signed-off-by: Eamonn Mansour <[email protected]>

* Add auto-generated Dex gRPC client

Signed-off-by: Eamonn Mansour <[email protected]>

* Add gRPC client and DTO

Signed-off-by: Eamonn Mansour <[email protected]>

* Add POST /auth/clients route

Signed-off-by: Eamonn Mansour <[email protected]>

* Store state param in session, add callback URL to redirect on auth completion, other tweaks

Signed-off-by: Eamonn Mansour <[email protected]>

* Update openapi.yaml

Signed-off-by: Eamonn Mansour <[email protected]>

* Add /auth/clients to OpenAPI docs, preserve query parameters in the callback URL provided by the user

Signed-off-by: Eamonn Mansour <[email protected]>

* Reuse Dex gRPC stub

Signed-off-by: Eamonn Mansour <[email protected]>

* Download Dex gRPC .proto file in gradle build

Signed-off-by: Eamonn Mansour <[email protected]>

* Add more error handling when getting OIDC config

Signed-off-by: Eamonn Mansour <[email protected]>

* Add logging and don't follow redirect when getting redirect URL

Signed-off-by: Eamonn Mansour <[email protected]>

* Add external API server URL as an env variable to get auth callback URL

Signed-off-by: Eamonn Mansour <[email protected]>

* Move callback route URL retrieval into auth callback route

Signed-off-by: Eamonn Mansour <[email protected]>

* Use consistent casing in auth responses

Signed-off-by: Eamonn Mansour <[email protected]>

* Bump GSON to 2.10.1

Signed-off-by: Eamonn Mansour <[email protected]>

* Remove extra logging and general tidy-up

Signed-off-by: Eamonn Mansour <[email protected]>

* Add unit tests for Dex gRPC client class

Signed-off-by: Eamonn Mansour <[email protected]>

* Refactor the initialisation of the oidcProvider and dexGrpcClient fields in the auth servlet

Signed-off-by: Eamonn Mansour <[email protected]>

* Add example errors to openapi.yaml

Signed-off-by: Eamonn Mansour <[email protected]>

* Fix formatting

Signed-off-by: Eamonn Mansour <[email protected]>

* Move env variable names into separate class, bump gson to 2.10.1

Signed-off-by: Eamonn Mansour <[email protected]>

* Add sequence diagrams for various auth flows

Signed-off-by: Eamonn Mansour <[email protected]>

* Add more logging, use single returns in methods where possible

Signed-off-by: Eamonn Mansour <[email protected]>

* Resolve review comments

Signed-off-by: Eamonn Mansour <[email protected]>

* Empty commit to kick off build

Signed-off-by: Eamonn Mansour <[email protected]>

---------

Signed-off-by: Eamonn Mansour <[email protected]>
  • Loading branch information
eamansour authored Jan 15, 2024
1 parent db03237 commit 6641d69
Show file tree
Hide file tree
Showing 39 changed files with 2,838 additions and 618 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ docs/generated/

# A folder scripts can put things into without fear of them being checked-in.
temp/
*.proto
24 changes: 24 additions & 0 deletions docs/images/authentication/cli-auth-refresh.plantuml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@startuml cli-auth-refresh
title "Authentication flow to get a new JWT for the Galasa CLI tool using a refresh token"

actor User
participant GalasaCLI as "Galasa CLI"
participant AuthAPI as "Auth API"
participant Dex

User -> GalasaCLI: Runs "galasactl auth login"
activate GalasaCLI

GalasaCLI -> AuthAPI: POST /auth (client_id, client_secret, refresh_token)
activate AuthAPI

AuthAPI -> Dex: POST /token (client_id, client_secret, refresh_token)
activate Dex
Dex --> AuthAPI: Success response (JWT, refresh token)
deactivate Dex
AuthAPI --> GalasaCLI: Success response (JWT, refresh token)
deactivate AuthAPI

GalasaCLI --> User: Stores the JWT in GALASA_HOME/bearer-token.json
deactivate GalasaCLI
@enduml
Binary file added docs/images/authentication/cli-auth-refresh.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions docs/images/authentication/initial-auth-flow.plantuml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
@startuml initial-auth-flow
title "Authentication flow when logging in to the Galasa Ecosystem for the first time"

actor User
participant WebUI as "Web UI"
participant AuthAPI as "Auth API"
participant Dex

User -> WebUI: Navigates to the web UI
activate WebUI

WebUI -> AuthAPI: GET /auth?client_id=galasa-webui&callback_url=http://webui-hostname/callback
activate AuthAPI
note left
This GET /auth request uses the
static client ID for the web UI
that was configured into Dex.
end note

AuthAPI -> Dex: GET /auth?client_id=galasa-webui&scope=...&state=somestate&redirect_uri=http://galasa-api/auth/callback
activate Dex
Dex --> AuthAPI: Redirect to /auth/callback?code=someauthcode&state=somestate
deactivate Dex
AuthAPI --> WebUI: Redirect to http://webui-hostname/callback?code=someauthcode
deactivate AuthAPI
note left
The redirect's location is the
same "callback_url" provided in
the initial GET /auth request.
end note

WebUI -> AuthAPI: POST /auth (client_id, client_secret, code)
activate AuthAPI
AuthAPI -> Dex: POST /token (client_id, client_secret, code)
activate Dex
Dex --> AuthAPI: Success response (JWT, refresh token)
deactivate Dex
AuthAPI --> WebUI: Success response (JWT, refresh token)
deactivate AuthAPI

WebUI --> User: Displays the web UI's landing page
deactivate WebUI
@enduml
Binary file added docs/images/authentication/initial-auth-flow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions docs/images/authentication/personal-access-token-flow.plantuml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@startuml personal-access-token-flow
title "Authentication flow when requesting a new personal access token"

actor User
participant WebUI as "Web UI"
participant AuthAPI as "Auth API"
participant Dex

User -> WebUI: Requests personal access token
activate WebUI

WebUI -> AuthAPI: POST /auth/clients with "Authorization: Bearer <JWT>" header
activate AuthAPI
AuthAPI -> AuthAPI: Check "Authorization" header contains a valid JWT
AuthAPI -> Dex: gRPC call to createClient()
activate Dex
Dex --> AuthAPI: Success response (client_id, client_secret)
deactivate Dex
AuthAPI --> WebUI: Success response (client_id, client_secret)
deactivate AuthAPI

WebUI -> AuthAPI: GET /auth?client_id=myclient&callback_url=http://webui-hostname/callback
activate AuthAPI
note left
The following is identical to the
initial authentication flow, but
the client_id used will be the ID
of the newly created Dex client.
end note

AuthAPI -> Dex: GET /auth?client_id=myclient&scope=...&state=somestate&redirect_uri=http://galasa-api/auth/callback
activate Dex
Dex --> AuthAPI: Redirect to /auth/callback?code=someauthcode&state=somestate
deactivate Dex
AuthAPI --> WebUI: Redirect to http://webui-hostname/callback?code=someauthcode
deactivate AuthAPI
note left
The redirect's location is the
same "callback_url" provided
in the GET /auth request.
end note

WebUI -> AuthAPI: POST /auth (client_id, client_secret, code)
activate AuthAPI
AuthAPI -> Dex: POST /token (client_id, client_secret, code)
activate Dex
Dex --> AuthAPI: Success response (JWT, refresh token)
deactivate Dex
AuthAPI --> WebUI: Success response (JWT, refresh token)
deactivate AuthAPI

WebUI --> User: Displays personal access token details
deactivate WebUI
@enduml
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 53 additions & 29 deletions galasa-parent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,6 @@ plugins {
// It is also read by other build scrips as required.
version = "0.31.0"

repositories {
gradlePluginPortal()
mavenCentral()
maven {
url "https://development.galasa.dev/main/maven-repo/obr/"
}
mavenLocal()
}

signing {
sign publishing.publications
}
Expand All @@ -34,6 +25,21 @@ dependencies {
compileOnly 'io.swagger.codegen.v3:swagger-codegen-cli:3.0.41'
}

allprojects {
tasks.withType(Javadoc) {
options.addStringOption('Xdoclint:none', '-quiet')
}

repositories {
gradlePluginPortal()
mavenCentral()
mavenLocal()
maven {
url "https://development.galasa.dev/main/maven-repo/obr/"
}
}
}

subprojects {
apply plugin: 'jacoco'
test {
Expand All @@ -44,11 +50,35 @@ subprojects {
}
}

//----------------------------------------------------------
// Download the Dex gRPC API .proto file
//----------------------------------------------------------
def protoDirPath = "$rootDir/dev.galasa.framework.api.authentication/src/main/proto"
def protoDir = new File(protoDirPath)
def dexProtoFile = new File("$protoDirPath/dex.proto")
task downloadDexProto() {
// If the .proto file already exists, we don't need to do anything
if (!dexProtoFile.exists()) {

// Make sure the proto directory exists so that the protobuf plugin works
if (!protoDir.exists()) {
protoDir.mkdirs()
}

// Download Dex's api.proto file from GitHub
new URL("https://raw.githubusercontent.com/dexidp/dex/v2.37.0/api/v2/api.proto").withInputStream{
inputStream -> dexProtoFile.withOutputStream{ outputStream -> outputStream << inputStream }
}
}
}

// Make sure the .proto file has been downloaded before building the framework
build.dependsOn downloadDexProto

task downloadDependencies(type: Copy) {
// Download the dependencies onto the local disk.
from configurations.compileClasspath
into 'build/dependencies'
dependsOn configurations.compileClasspath
}

gradle.taskGraph.beforeTask { Task task ->
Expand All @@ -63,25 +93,19 @@ gradle.taskGraph.afterTask { Task task, TaskState state ->
println " -> took " + mins + ( ( 1 == mins ) ? " min " : " mins " ) + sec + ( ( 1 == sec ) ? " sec" : " secs" )
}

allprojects {
tasks.withType(Javadoc) {
options.addStringOption('Xdoclint:none', '-quiet')
}
}

//---------------------------------------------------------------
// We need to gather the release and packaging metadata from each
// sub-project, to generate a release.yaml document which can act
// as a manifest for this component.
//
// The OSGi bundles in this project are all in one of two groups:
// - The 'framework'
// or
// or
// - The 'api'
//
// Each module is examined, and contributes it's metadata to one
// of two manifest files. Both manifest files are then combined
// into an overall manifest file ready to be published to a
// into an overall manifest file ready to be published to a
// maven repository.
//
// At a later time, the OBR project will draw-down the manifest
Expand All @@ -105,7 +129,7 @@ def overallHeader = """#
#
# WARNING
#
# This file is periodically re-generated from the contents of
# This file is periodically re-generated from the contents of
# the repository, so don't make changes here manually please.
# -----------------------------------------------------------
Expand All @@ -130,7 +154,7 @@ api:


//----------------------------------------------------------
// Flushes any existing content on the specified path, and
// Flushes any existing content on the specified path, and
// creates a new file, containing the header text.
//----------------------------------------------------------
def prepareGeneratedFile(path , header) {
Expand All @@ -147,7 +171,7 @@ def prepareGeneratedFile(path , header) {
}

//----------------------------------------------------------
// Prepare the overall manifest, and a manifest for each of
// Prepare the overall manifest, and a manifest for each of
// the 'framework' and 'api' collections of bundles.
//----------------------------------------------------------
task buildReleaseYamlPrepare() {
Expand All @@ -157,15 +181,15 @@ task buildReleaseYamlPrepare() {
if ( !buildDir.exists() ) {
buildDir.mkdirs()
}

prepareGeneratedFile(overallManifestFilePath,overallHeader)
prepareGeneratedFile(frameworkManifestFilePath,frameworkHeader)
prepareGeneratedFile(apiManifestFilePath,apiHeader)
}
}

//----------------------------------------------------------
// Allow each subproject to contribute to one of the manifest
// Allow each subproject to contribute to one of the manifest
// collectons.
//----------------------------------------------------------
task buildReleaseYamlSubprojects() {
Expand All @@ -177,8 +201,8 @@ task buildReleaseYamlSubprojects() {
// Each sub-project will set the values...
projectName = '' // The name of the bundle.
includeInOBR = '' // Is the bundle included in the uber-obr ?
includeInMVP = ''
includeInBOM = ''
includeInMVP = ''
includeInBOM = ''
includeInJavadoc = '' // Is the component displayed to users on the public javadoc site ?
includeInIsolated = '' // Is the component included in the bundles shipped as part of the isolated build ?
includeInCodeCoverage = ''
Expand Down Expand Up @@ -230,8 +254,8 @@ task buildReleaseYamlSubprojects() {
}

//----------------------------------------------------------
// Once we've collected all the metadata into either the
// 'framework' or 'api' collections of bundle information,
// Once we've collected all the metadata into either the
// 'framework' or 'api' collections of bundle information,
// Combine them into the 'overall' manifest file.
//----------------------------------------------------------
task buildReleaseYamlFinalise() {
Expand All @@ -258,7 +282,7 @@ def myReleaseYaml = artifacts.add('release_metadata', file(overallManifestFilePa
// Publish the release.yaml as a maven artifact.
// Note: The maven co-ordinates are versioned using the version for this bundle.
publishing {

publications {

// Publish the component manifest/release.yaml
Expand Down Expand Up @@ -302,7 +326,7 @@ publishing {
repositories {
maven {
url = "$targetMaven"

if ("$targetMaven".startsWith('http')) {
credentials {
username System.getenv('MAVENUSERNAME')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ dependencies {
implementation 'org.osgi:org.osgi.service.component.annotations:1.3.0'
implementation 'javax.validation:validation-api:2.0.1.Final'
implementation 'commons-logging:commons-logging:1.2'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'io.prometheus:simpleclient:0.6.0'

testImplementation 'junit:junit:4.13.1'
testImplementation 'org.mockito:mockito-core:3.1.0'
testImplementation 'org.awaitility:awaitility:3.0.0'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
plugins {
id 'biz.aQute.bnd.builder'
id 'galasa.api.server'
id "com.google.protobuf" version "0.9.4"

id 'java'
}

description = 'Galasa Authentication API'

version = '0.30.0'
version = '0.31.0'

dependencies {
implementation project(':dev.galasa.framework')
implementation project(':dev.galasa.framework.api.common')

implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'dev.galasa:com.auth0.jwt:4.4.0'

implementation 'dev.galasa:io.grpc.java:1.60.0'
compileOnly 'org.apache.tomcat:annotations-api:6.0.53'

testImplementation(testFixtures(project(':dev.galasa.framework.api.common')))
}

protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.25.1"
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.60.0'
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}

// Note: These values are consumed by the parent build process
// They indicate which packages of functionality this OSGi bundle should be delivered inside,
// or referenced from.
Expand Down
Loading

0 comments on commit 6641d69

Please sign in to comment.