Quarkus Security 2 - HTTP Basic Auth dengan JDBC

Kita akan membahas  bagaimana Quarkus  dapat menggunakan database untuk menyimpan provider identitas pengguna dari database ke HTTP Basic Auth.

1. Persiapan

Untuk menyelesaikan panduan ini, kita memerlukan:

  • Waktu sekitar 15 menit
  • IDE IntelliJ IDEA
  • JDK 17+ yang terinstal dengan JAVA_HOME yang dikonfigurasi dengan benar
  • Apache Maven 3.9.9
  • Opsional: Quarkus CLI jika ingin menggunakannya
  • Opsional: Mandrel atau GraalVM yang terinstal dan dikonfigurasi dengan benar jika  ingin membangun executable native (atau Docker jika  menggunakan build kontainer native)

2. Arsitektur

Dalam contoh ini, kita membangun sebuah mikroservis yang sangat sederhana yang menawarkan tiga endpoint:

  • /api/public
  • /api/users/me
  • /api/admin

Endpoint /api/public dapat diakses secara anonim. Endpoint /api/admin dilindungi dengan RBAC (Role-Based Access Control) di mana hanya pengguna yang diberikan peran admin yang dapat mengaksesnya. Pada endpoint ini, kita menggunakan anotasi @RolesAllowed untuk secara deklaratif menegakkan batasan akses. Endpoint /api/users/me juga dilindungi dengan RBAC, di mana hanya pengguna yang diberikan peran user yang dapat mengaksesnya. Sebagai respons, endpoint ini mengembalikan dokumen JSON yang berisi detail tentang pengguna.

3. Solusi

Kita dapat langsung menggunakan contoh yang telah selesai dengan clone repositori Git berikut  dengan perintah:

git clone https://github.com/quarkusio/quarkus-quickstarts.git
Clone via IntelliJ IDEA
Project setelah di Load ke IDE
Loaded Projects

4. Persiapkan Database Table

 

CREATE TABLE test_user (

id INT,

username VARCHAR(255),

password VARCHAR(255),

role VARCHAR(255)

);


INSERT INTO test_user (id, username, password, role) VALUES (1, 'admin', '$2a$10$Uc.SZ0hvGJQlYdsAp7be1.lFjmOnc7aAr4L0YY3/VN3oK.F8zJHRG', 'admin');

INSERT INTO test_user (id, username, password, role) VALUES (2, 'user', '$2a$10$Uc.SZ0hvGJQlYdsAp7be1.lFjmOnc7aAr4L0YY3/VN3oK.F8zJHRG', 'user');


SQL Query data Users

 

Data users di database

Kita menggunakan PostgreSQL sebagai penyimpanan data, dan kita menginisialisasi basis data dengan pengguna dan role. Kita akan menggunakan versi kata sandi yang di-hash dan disalin sebagai kata sandi dalam contoh ini. Kita dapat menggunakan kelas BcryptUtil untuk menghasilkan kata sandi dalam format Modular Crypt Format (MCF).

5. Hasil di Browser 

User: admin/user, pass: password. 

User login user di database
User berhasil Login

Konfigurasi Keamanan JDBC Quarkus

Properti Utama

Properti Tipe Default Deskripsi
quarkus.security.jdbc.realm-name string Quarkus Nama realm untuk otentikasi
quarkus.security.jdbc.enabled boolean false Mengaktifkan penyimpanan properti
quarkus.security.jdbc.principal-query.sql string - Kueri SQL untuk menemukan password
quarkus.security.jdbc.principal-query.datasource string - Sumber data untuk kueri

Pemetaan Password Clear Text

Properti Tipe Default Deskripsi
quarkus.security.jdbc.principal-query.clear-password-mapper.enabled boolean false Aktifkan pemetaan password clear text
quarkus.security.jdbc.principal-query.clear-password-mapper.password-index int 1 Indeks kolom password

Pemetaan Bcrypt Password

Properti Tipe Default Deskripsi
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabled boolean false Aktifkan pemetaan password bcrypt
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-index int 0 Indeks kolom hash password
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.hash-encoding string base64 Encoding hash (BASE64/HEX)
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-index int -1 Indeks kolom salt
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-encoding string base64 Encoding salt (BASE64/HEX)
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.iteration-count-index int -1 Indeks jumlah iterasi

Code dari Applikasi Diatas

1. Class PublicResource (Controller)

Kode ini mendefinisikan dua endpoint: satu untuk mengakses informasi publik dan satu lagi untuk mendapatkan nama pengguna yang sedang masuk. Endpoint ini dapat digunakan dalam aplikasi yang memerlukan otentikasi dan otorisasi, dengan memberikan informasi yang sesuai berdasarkan status login pengguna.

package org.acme.security.jdbc;

import jakarta.annotation.security.PermitAll;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/api/public")
public class PublicResource {

@GET
@PermitAll
@Produces(MediaType.TEXT_PLAIN)
public String publicResource() {
return "public";
}
}

Penjelasan Kode Class PublicResource:

  1. Class Declaration:

    • @Path("/api/public"): Menentukan bahwa kelas ini akan menangani permintaan yang dimulai dengan /api/public.
  2. Endpoint /api/public:

    • @GET: Menandakan bahwa metode ini akan menangani permintaan HTTP GET.
    • @Produces(MediaType.TEXT_PLAIN): Menentukan bahwa metode ini akan mengembalikan respons dalam format teks biasa.
    • public String publicResource(): Metode ini mengembalikan string "public" ketika endpoint ini diakses.
  3. Endpoint /api/public/me:

    • @GET: Menandakan bahwa metode ini juga akan menangani permintaan HTTP GET.
    • @Path("/me"): Menentukan path tambahan untuk endpoint ini.
    • @Produces(MediaType.TEXT_PLAIN): Menentukan bahwa metode ini juga akan mengembalikan respons dalam format teks biasa.
    • public String me(@Context SecurityContext securityContext): Metode ini menerima objek SecurityContext sebagai parameter, yang memberikan informasi tentang keamanan saat ini.
    • Principal user = securityContext.getUser Principal();: Mengambil objek Principal yang mewakili pengguna yang sedang masuk.
    • return user != null ? user.getName() : "<not logged in>";: Mengembalikan nama pengguna jika pengguna terautentikasi, atau string "<not logged in>" jika tidak ada pengguna yang terautentikasi.

2. Kode Class AdminResource (Controller)

package org.acme.security.jdbc;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/api/admin")
public class AdminResource {

@GET
@RolesAllowed("admin")
@Produces(MediaType.TEXT_PLAIN)
public String adminResource() {
return "admin";
}
}

Penjelasan Kode AdminResource:

  1. Anotasi Kelas:

    • @Path("/api/admin") - Menentukan base path untuk semua endpoint di kelas ini artinya semua endpoint akan diawali dengan /api/admin
  2. Endpoint Admin:

    Hanya user dengan role "admin" yang bisa mengakses endpoint ini Jika user tanpa role "admin" mencoba akses akan mendapatkan response HTTP 403 Forbidden, dan jika user belum login mencoba akses akan mendapatkan response HTTP 401 Unauthorized   Detail Implementasi:

    • @GET - Menandakan ini endpoint metode HTTP GET
    • @RolesAllowed("admin") - Membatasi akses hanya untuk user dengan role "admin"
      • Ini adalah bagian dari RBAC (Role-Based Access Control)
    • @Produces(MediaType.TEXT_PLAIN) - Menentukan response berupa teks plain (bukan JSON/XML)
    • Method mengembalikan string sederhana "admin"
     

3. Kode Class UserResource (Controller)

package org.acme.security.jdbc;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;

@Path("/api/users")
public class UserResource {

@GET
@RolesAllowed("user")
@Path("/me")
@Produces(MediaType.TEXT_PLAIN)
public String me(@Context SecurityContext securityContext) {
return securityContext.getUserPrincipal().getName();
}
}

Penjelasan Mendetail:

  1. Endpoint /me:

    • @GET - Endpoint HTTP GET
    • @RolesAllowed("user") - Hanya boleh diakses oleh user dengan role "user"
    • @Path("/me") - Membentuk endpoint lengkap /api/users/me
    • Parameter SecurityContext - Memberikan akses ke informasi keamanan Fungsi Utama: 
    • Mengembalikan nama user yang sedang login
    • Menggunakan getUserPrincipal().getName() untuk mendapatkan identitas user
    • Tidak perlu pengecekan null karena: Anotasi @RolesAllowed sudah memastikan user terautentikasi
    • Jika belum login, akan otomatis mengembalikan 401 Unauthorized 

    Alur Kerja:

- Client mengakses /api/users/me
- Sistem memverifikasi: Apakah user sudah login?
- Apakah user memiliki role "user"? 
- Jika valid: Mengembalikan nama user 
- Jika tidak valid: Mengembalikan kode error (401 atau 403)

Best Practice yang Diterapkan:

  1. Pembatasan akses dengan @RolesAllowed
  2. Penggunaan SecurityContext untuk mendapatkan info user
  3. Organisasi path API yang konsisten
  4. Nama endpoint yang deskriptif (/me untuk data user yang login)

4. Kode Class User (Entity Model)

Model entitas User ini menyediakan cara yang efisien untuk mengelola pengguna dalam aplikasi Quarkus dengan menggunakan Elytron untuk keamanan dan Hibernate ORM untuk interaksi basis data. Dengan metode add, kita dapat dengan mudah menambahkan pengguna baru dengan kata sandi yang aman. Endpoint REST yang disediakan memungkinkan pengguna untuk mendaftar dengan mudah. 

package org.acme.elytron.security.jpa;

import jakarta.persistence.Entity;
import jakarta.persistence.Table;

import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.quarkus.security.jpa.Password;
import io.quarkus.security.jpa.Roles;
import io.quarkus.security.jpa.UserDefinition;
import io.quarkus.security.jpa.Username;

@Entity
@Table(name = "test_user")
@UserDefinition
public class User extends PanacheEntity {
@Username
public String username;
@Password
public String password;
@Roles
public String role;

/**
* Adds a new user in the database
* @param username the user name
* @param password the unencrypted password (it will be encrypted with bcrypt)
* @param role the comma-separated roles
*/
public static void add(String username, String password, String role) {
User user = new User();
user.username = username;
user.password = BcryptUtil.bcryptHash(password);
user.role = role;
user.persist();
}
}

Penjelasan Kode:

  1. Paket dan Impor:

    • Kode ini berada dalam paket org.acme.elytron.security.jpa.
    • Mengimpor berbagai kelas dari Jakarta Persistence, Quarkus Elytron, dan Hibernate ORM.
  2. Anotasi Entitas:

    • @Entity: Menandakan bahwa kelas ini adalah entitas JPA yang akan dipetakan ke tabel dalam basis data.
    • @Table(name = "test_user"): Menentukan nama tabel dalam basis data yang akan digunakan untuk menyimpan entitas ini.
    • @User Definition: Menandakan bahwa kelas ini mendefinisikan pengguna untuk keperluan otentikasi.
  3. Atribut Kelas:

    • @Username: Menandakan bahwa atribut username adalah nama pengguna yang digunakan untuk otentikasi.
    • @Password: Menandakan bahwa atribut password adalah kata sandi yang akan di-hash.
    • @Roles: Menandakan bahwa atribut role menyimpan peran pengguna.
  4. Metode Statis add:

    • Metode ini digunakan untuk menambahkan pengguna baru ke dalam basis data.
    • Menerima parameter username, password, dan role.
    • Kata sandi yang diterima akan di-hash menggunakan BcryptUtil.bcryptHash(password).
    • Setelah itu, objek User baru akan disimpan ke dalam basis data dengan memanggil user.persist().

 

Konfigurasi dari Aplikasi

1. Mengonfigurasi Aplikasi

Ekstensi elytron-security-jdbc memerlukan setidaknya satu datasource untuk mengakses basis data.

Berikut adalah konfigurasi yang diperlukan untuk menggunakan PostgreSQL sebagai penyimpanan identitas (application.properties):

quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=postgres
quarkus.datasource.password=postgres
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/elytron_security_jdbc

Penjelasan Konfigurasi:

  1. quarkus.datasource.db-kind=postgresql

    • Menentukan jenis basis data yang digunakan, dalam hal ini PostgreSQL.
  2. quarkus.datasource.username=quarkus

    • Menentukan nama pengguna yang digunakan untuk mengakses basis data.
  3. quarkus.datasource.password=quarkus

    • Menentukan kata sandi untuk pengguna yang ditentukan.
  4. quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/elytron_security_jdbc

    • Menentukan URL JDBC untuk menghubungkan aplikasi ke basis data PostgreSQL. Dalam hal ini, nama basis data yang digunakan adalah elytron-security-jdbc.

2. Konteks Penggunaan

Dalam konteks ini, kita menggunakan PostgreSQL sebagai penyimpanan identitas, dan kita menginisialisasi basis data dengan pengguna dan peran. Kita akan menggunakan versi kata sandi yang di-hash dan disalin sebagai kata sandi dalam contoh ini. Kita dapat menggunakan kelas BcryptUtil untuk menghasilkan kata sandi dalam format Modular Crypt Format (MCF).

3. Contoh Penggunaan BcryptUtil

Untuk menghasilkan kata sandi yang aman, dapat menggunakan kelas BcryptUtil seperti berikut:

import org.jboss.security.bcrypt.BcryptUtil;
...

Penjelasan:

  • Kode di atas menggunakan BcryptUtil untuk menghasilkan hash dari kata sandi yang diberikan.
  • Hash ini dapat disimpan dalam basis data sebagai kata sandi yang aman.

Dengan konfigurasi ini, kita siap untuk menggunakan PostgreSQL sebagai penyimpanan identitas dengan dukungan untuk otentikasi dan otorisasi berbasis peran. Pastikan untuk menginisialisasi basis data dengan pengguna dan peran yang sesuai agar aplikasi dapat berfungsi dengan baik.

 

Saat ini kita bisa mengonfigurasi Elytron JDBC Realm.

Konfigurasi Elytron  - application.properties 

quarkus.security.jdbc.enabled=true
quarkus.security.jdbc.principal-query.sql=SELECT u.password, u.role FROM test_user u WHERE u.username=?
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabled=true
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-index=1
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-index=-1
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.iteration-count-index=-1
quarkus.security.jdbc.principal-query.attribute-mappings.0.index=2
quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups

 

Berikut adalah terjemahan dan susunan dari konfigurasi yang Anda berikan:

  • quarkus.security.jdbc.enabled=true: Mengaktifkan realm JDBC untuk keamanan.

  • quarkus.security.jdbc.principal-query.sql=SELECT u.password, u.role FROM test_user u WHERE u.username=?: Mendefinisikan kueri SQL utama untuk otentikasi. Kueri ini mengambil kata sandi (password) dan peran (role) dari tabel test_user berdasarkan username.

  • quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabled=true: Mengaktifkan pemetaan kata sandi dengan algoritma bcrypt. Ini penting untuk membandingkan kata sandi yang dimasukkan pengguna dengan hash kata sandi yang tersimpan.

  • quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-index=1: Menunjukkan bahwa kolom kata sandi (u.password) berada pada indeks 1 dari hasil kueri SQL.

  • quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-index=-1: Menunjukkan bahwa salt (nilai acak yang ditambahkan ke kata sandi sebelum di-hash) tidak disimpan di kolom terpisah. Nilai -1 berarti salt disimpan sebagai bagian dari hash kata sandi dalam format Modular Crypt Format (MCF).

  • quarkus.security.jdbc.principal-query.bcrypt-password-mapper.iteration-count-index=-1: Menunjukkan bahwa jumlah iterasi tidak disimpan di kolom terpisah. Nilai -1 berarti nilai ini juga tersimpan di dalam hash kata sandi MCF.

  • quarkus.security.jdbc.principal-query.attribute-mappings.0.index=2: Memetakan kolom pada indeks 2 dari hasil kueri (u.role).

  • quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups: Mengaitkan kolom peran (u.role) ke atribut groups yang digunakan dalam representasi user. Ini memungkinkan Elytron untuk memuat peran pengguna setelah otentikasi berhasil.

     

Penjelasan dan Konteks Tambahan

Ekstensi elytron-security-jdbc memerlukan setidaknya satu kueri utama (principal query) untuk mengotentikasi dan memuat identitas pengguna. Kueri ini harus berupa pernyataan SQL berparameter (menggunakan ?) dan mengembalikan kata sandi pengguna beserta informasi tambahan yang Anda butuhkan (seperti peran).

Password Mapper dikonfigurasi untuk menunjukkan posisi kolom kata sandi di hasil kueri. Karena nilai salt-index dan iteration-count-index diatur ke -1, hash kata sandi disimpan dalam format Modular Crypt Format (MCF). Anda bisa mengubahnya jika salt dan jumlah iterasi disimpan di kolom yang berbeda.

Properti attribute-mappings digunakan untuk menghubungkan kolom-kolom yang dipilih dalam kueri (u.role) dengan atribut yang akan diwakili dalam objek user (groups).

Penting: Dalam konfigurasi principal-query, properti index dimulai dari 1, bukan 0.

 

 

Sumber Belajar 

https://quarkus.io/guides/security-jdbc  

 

Comments

Popular posts from this blog

Numpang Kerja Remote dari Bandung Creative Hub

Numpang Kerja Remote dari Bandung Digital Valley

Cara Decompile berkas Dex dan Apk Android