Secure Grafana with JWT & Java: A Practical Guide

Secure Grafana with JWT & Java: A Practical Guide
grafana jwt java

Introduction: Fortifying Your Observability Stack

In the intricate landscape of modern IT infrastructure, robust monitoring and observability are not merely luxuries but fundamental necessities. Grafana stands as a cornerstone in this domain, providing powerful, customizable dashboards that transform raw data into actionable insights. From visualizing system metrics and application performance to tracking business KPIs, Grafana empowers teams to understand the health and behavior of their systems at a glance. However, the very power that makes Grafana indispensable also makes it a high-value target for unauthorized access. Exposing sensitive operational data without stringent security measures can lead to severe breaches, compromising system integrity, customer data, and regulatory compliance.

Traditional authentication methods, while functional, often introduce complexities, particularly in distributed environments or when integrating with microservices architectures. This is where JSON Web Tokens (JWT) emerge as a compelling, modern alternative. JWTs offer a stateless, self-contained, and compact way to securely transmit information between parties, ideal for single sign-on (SSO) scenarios and securing API endpoints. When combined with the robust capabilities of Java for building secure backend services, JWTs provide a powerful paradigm for managing user identities and access control.

This comprehensive guide delves into the practicalities of securing Grafana using a custom JWT-based authentication system built with Java. We will explore the architectural considerations, delve into the implementation details of a Java authentication service, and meticulously configure Grafana to leverage this modern security mechanism. Furthermore, we will highlight the critical role of an API gateway in augmenting this security, providing a centralized point for authentication, authorization, and traffic management. By the end of this article, you will possess a profound understanding and a detailed roadmap to implementing a highly secure, scalable, and manageable Grafana environment, ensuring that your valuable operational data remains protected while remaining accessible to authorized personnel.

Section 1: Understanding the Landscape – Grafana, JWT, and Java

Before embarking on the implementation journey, it's crucial to establish a solid understanding of the core components involved: Grafana, JSON Web Tokens (JWT), and the Java ecosystem. Each plays a distinct yet interconnected role in constructing a secure observability platform.

1.1 Grafana: The Observability Powerhouse

Grafana has evolved from a simple visualization tool into a sophisticated, open-source platform for data analysis and monitoring. Its strength lies in its ability to connect to a multitude of data sourcesβ€”ranging from Prometheus and InfluxDB to SQL databases, Elasticsearch, and cloud monitoring servicesβ€”and present this data through highly customizable and interactive dashboards. Developers, operations teams, and business analysts alike rely on Grafana to:

  • Create Dynamic Dashboards: Build rich, real-time dashboards with various visualization panels (graphs, tables, heatmaps, singlestats) to monitor system health, application performance, and user experience. These dashboards can be drilled down, filtered, and aggregated, offering deep insights into complex systems.
  • Consolidate Data Sources: Act as a unified interface to pull data from disparate systems, providing a single pane of glass for all monitoring needs, reducing context switching and improving operational efficiency.
  • Set Up Alerts and Notifications: Configure threshold-based alerts on critical metrics, notifying teams via integrated channels like Slack, PagerDuty, email, or webhooks when predefined conditions are met. This proactive approach helps in identifying and resolving issues before they impact services.
  • Enable Collaborative Workflows: Facilitate team collaboration by allowing users to share dashboards, annotations, and comments, fostering a shared understanding of system status and facilitating quicker incident response.
  • Extend Functionality with Plugins: Its extensive plugin ecosystem allows users to connect to new data sources, add custom visualizations, and integrate with external systems, continuously expanding its capabilities.

From an architectural standpoint, Grafana is typically deployed as a standalone web application. It primarily consists of a Go backend that handles data source queries, authentication, user management, and API interactions, complemented by a React-based frontend for rendering the user interface. While highly versatile, the security of Grafana hinges on how access to its API and dashboards is controlled. Improperly secured Grafana instances can expose sensitive infrastructure metrics, customer data, or even provide pathways for lateral movement within an organization's network, underscoring the paramount importance of robust authentication.

1.2 JSON Web Tokens (JWT): A Deep Dive

JSON Web Tokens (JWTs) represent a modern, open standard (RFC 7519) for securely transmitting information between parties as a JSON object. They are particularly well-suited for authentication and authorization in stateless API architectures, microservices, and single-page applications. The core appeal of JWTs lies in their self-contained nature and the ability to verify their integrity using digital signatures.

A JWT consists of three parts, separated by dots, each base64url-encoded:

  1. Header: Typically consists of two parts: the type of the token (JWT) and the signing algorithm being used (e.g., HMAC SHA256 or RSA). json { "alg": "HS256", "typ": "JWT" }
  2. Payload (Claims): Contains the actual information about the entity (typically, the user) and additional data. Claims are statements about an entity (usually, the user) and additional metadata. There are three types of claims:
    • Registered Claims: Predefined claims that are not mandatory but are recommended to provide a set of useful, interoperable claims. Examples include iss (issuer), exp (expiration time), sub (subject), aud (audience).
    • Public Claims: These can be defined by those using JWTs. To avoid collisions, they should be defined in the IANA JSON Web Token Registry or be a URI that contains a collision-resistant namespace.
    • Private Claims: Custom claims created to share information between parties that agree on their usage. For instance, role or user_id. json { "sub": "user@example.com", "name": "John Doe", "iat": 1516239022, "exp": 1516242622, "role": "admin" }
  3. Signature: Created by taking the encoded header, the encoded payload, a secret key, and the algorithm specified in the header, and signing them. The signature is used to verify that the sender of the JWT is who it says it is and to ensure that the message hasn't been tampered with along the way.

How JWTs Work in Authentication:

  1. Authentication: A user logs in with credentials (username/password) to an authentication service (our Java backend).
  2. Token Issuance: Upon successful authentication, the service generates a JWT containing relevant user claims (e.g., user ID, roles, expiration time) and signs it with a secret key.
  3. Token Transmission: The JWT is sent back to the client (e.g., web browser) as part of the authentication response.
  4. Resource Access: The client stores the JWT (typically in local storage or an HttpOnly cookie) and includes it in subsequent requests to protected resources (e.g., Grafana API, Grafana dashboards), usually in the Authorization header as a Bearer token.
  5. Token Validation: The server (or an intermediary like an API gateway) receives the JWT, verifies its signature using the secret key, checks its expiration, and validates other claims. If valid, the request is authorized, and the user gains access.

Benefits of JWTs:

  • Statelessness: No session state needs to be maintained on the server, simplifying scalability for distributed systems.
  • Compactness: Small size allows for efficient transmission, often via URL or HTTP headers.
  • Self-contained: Contains all necessary user information, reducing the need for database lookups on every request.
  • Security: Signed tokens ensure integrity and authenticity.

Security Considerations for JWTs:

While powerful, JWTs are not without their vulnerabilities. It's crucial to: * Use Strong Signing Keys: Protect the secret key at all costs, as its compromise allows attackers to forge tokens. Rotate keys regularly. * Set Short Expiration Times: Limit the window of opportunity for attackers if a token is stolen. Combine with refresh tokens for a better user experience. * Implement Revocation: For critical actions or compromised accounts, have a mechanism (e.g., a blacklist) to invalidate tokens before their natural expiry. * Store Securely: Avoid storing JWTs in local storage due to XSS risks; HttpOnly cookies are generally preferred, especially for sensitive applications. * Always Use HTTPS: Prevent tokens from being intercepted in transit.

1.3 Java for Authentication Backends

Java, with its mature ecosystem, robust security features, and high performance, is an excellent choice for developing the authentication service that will issue and validate JWTs. Its platform independence, extensive libraries, and strong community support make it suitable for enterprise-grade applications.

Key Advantages of Java for this Application:

  • Powerful Frameworks: Frameworks like Spring Boot significantly accelerate development by providing convention-over-configuration, embedded servers, and a vast array of modules for web development, security, and data access. Micronaut and Quarkus offer similar benefits with a focus on microservices and cloud-native environments.
  • Robust Security Libraries: Spring Security is a de-facto standard for securing Java applications, offering comprehensive features for authentication, authorization, and protection against common web vulnerabilities. For JWT handling, libraries like jjwt (Java JWT) simplify token creation, parsing, and validation.
  • Scalability and Performance: Java applications can be highly performant and scalable, capable of handling a large volume of authentication requests, especially when deployed with modern cloud-native patterns.
  • Developer Productivity: Rich IDEs, debugging tools, and a mature build ecosystem (Maven, Gradle) contribute to efficient development and maintenance.

In our architecture, the Java backend service will serve several critical functions: 1. User Authentication: Verifying user credentials against a stored user database. 2. JWT Issuance: Generating signed JWTs upon successful authentication, embedding user-specific claims. 3. JWT Validation (Optional but Recommended): Providing an endpoint to validate incoming JWTs and return user details, which can be leveraged by an API gateway or Grafana itself. 4. User and Role Management: Managing users, their passwords (hashed, of course), and their associated roles, which will later map to Grafana permissions.

By combining Java's enterprise-grade capabilities with the stateless security model of JWTs, we can construct a highly effective and secure authentication layer for Grafana.

APIPark is a high-performance AI gateway that allows you to securely access the most comprehensive LLM APIs globally on the APIPark platform, including OpenAI, Anthropic, Mistral, Llama2, Google Gemini, and more.Try APIPark now! πŸ‘‡πŸ‘‡πŸ‘‡

Section 2: The Core Challenge – Grafana's Authentication Mechanisms

Integrating a custom JWT-based authentication system with Grafana requires a deep understanding of Grafana's existing authentication mechanisms. Grafana offers a variety of ways to authenticate users, each with its own strengths and use cases. Our goal is to find the most suitable method that allows us to seamlessly plug in our Java-based JWT solution.

2.1 Grafana's Native Authentication Options

Grafana is designed to be flexible, supporting a wide range of authentication providers out-of-the-box. These include:

  • Built-in Grafana Database: This is the default authentication method, where user accounts and passwords are stored directly within Grafana's internal SQLite, MySQL, or PostgreSQL database. It's simple for small deployments but lacks centralized user management capabilities for larger organizations.
  • LDAP/Active Directory: For enterprises with existing directory services, Grafana can integrate with LDAP or Active Directory, allowing users to log in with their corporate credentials. This centralizes user management but might not be ideal for external users or modern API-driven architectures.
  • OAuth (Generic OAuth, Google OAuth, GitHub OAuth, GitLab OAuth, etc.): Grafana supports various OAuth providers, enabling single sign-on (SSO) through popular identity services. The generic OAuth provider is particularly interesting as it allows integration with any OAuth2-compatible service, but it expects a specific OAuth flow and token structure.
  • SAML (Security Assertion Markup Language): Another enterprise-grade SSO solution, SAML is widely used for federated identity management, allowing users to authenticate once with an Identity Provider (IdP) and gain access to multiple Service Providers (SPs), including Grafana.
  • Reverse Proxy Authentication (Auth Proxy): This is a powerful and highly flexible method where Grafana trusts an upstream reverse proxy (like Nginx, Apache, or a dedicated API gateway) to handle authentication. The proxy authenticates the user and then forwards specific HTTP headers to Grafana containing the authenticated user's details (username, email, roles). Grafana then uses these headers to log the user in.
  • Anonymous Access: Allows unauthenticated users to view specific dashboards. This is generally used for public display boards and is not suitable for secure, personalized access.

Choosing the Right Strategy for JWT:

While OAuth seems like a natural fit for JWTs, Grafana's generic OAuth implementation expects a specific OAuth2 flow, which might require your Java service to act as a full-fledged OAuth2 Authorization Server. This adds complexity. For a custom JWT implementation where our Java service simply issues and validates tokens, the Reverse Proxy Authentication (Auth Proxy) method is often the most straightforward and robust approach.

2.2 The Reverse Proxy & Generic OAuth Approach for JWT

Given the nature of JWTs as self-contained tokens, we primarily have two strong contenders for integrating them with Grafana:

  1. Reverse Proxy Authentication (auth.proxy): This method involves placing a reverse proxy or API gateway in front of Grafana. The flow would be:Advantages: * Decoupling: Grafana doesn't need to understand JWT specifics; it simply trusts the upstream proxy. * Centralized Security: The API gateway handles the heavy lifting of token validation, making it a central enforcement point for all API traffic, not just Grafana. * Flexibility: Allows for complex logic within the proxy/ gateway for user mapping and role assignments.Disadvantages: * Requires an additional component (the proxy/ gateway) in the architecture. * Proper configuration of the proxy is critical for security.
    • The user authenticates with our custom Java authentication service, which issues a JWT.
    • The client (browser) stores this JWT.
    • For subsequent requests to Grafana, the client sends the JWT (e.g., in an Authorization header).
    • The reverse proxy/ API gateway intercepts this request.
    • Crucially, the proxy validates the JWT (either internally or by calling our Java service for validation).
    • If the JWT is valid, the proxy extracts user information (username, email, roles) from the token and injects it into specific HTTP headers (e.g., X-WEBAUTH-USER, X-WEBAUTH-EMAIL, X-Grafana-Org-Role).
    • The proxy then forwards the request, including these new headers, to Grafana.
    • Grafana, configured to trust these headers, reads the user information and logs the user in without needing to validate the JWT itself.
  2. Generic OAuth ([auth.generic_oauth]): If your Java service is designed to function as an OAuth2 Authorization Server, providing endpoints for authorization, token issuance, and user info retrieval, then Grafana's generic OAuth provider can be configured.Advantages: * Leverages a standardized protocol. * Grafana handles more of the flow internally.Disadvantages: * Requires a more complex OAuth2 implementation on the Java backend, including authorization endpoints, token endpoints, and user info endpoints. * Might be overkill if you simply want to issue and consume JWTs directly.
    • The user would initiate a login from Grafana, which redirects to your Java OAuth authorization endpoint.
    • Upon successful authentication, your Java service issues an authorization code, which Grafana exchanges for an access token (which could be a JWT).
    • Grafana then uses this access token to call your Java service's user info endpoint to retrieve user details.

For most custom JWT integrations with Grafana, especially when aiming for simplicity in Grafana's configuration and centralizing authentication logic, the Reverse Proxy Authentication method paired with an API gateway or a reverse proxy like Nginx is often the preferred and most robust choice. It cleanly separates concerns and allows the Java service to focus solely on user authentication and JWT issuance, while the gateway handles the token validation and header injection. This architectural choice forms the basis of our practical guide.

Section 3: Designing the Secure Architecture

A well-thought-out architectural design is the cornerstone of any secure and scalable system. For securing Grafana with JWT and Java, we envision a layered architecture that leverages specialized components to handle different aspects of the authentication and authorization flow. This section outlines the high-level design and the key components involved, emphasizing security at each layer.

3.1 High-Level Architecture Diagram (Conceptual)

Imagine a flow where a user interacts with a client application, authenticates, and then accesses Grafana. The architectural components would interact as follows:

+------------------+     (1) User Login Request        +-----------------------+
|    User's        |---------------------------------->|   Java Authentication |
|    Browser       |<----------------------------------|      Service (JWT     |
|   (Client App)   |      (2) JWT Issued & Stored      |      Issuer/Validator) |
+------------------+                                   +-----------------------+
        |                                                            ^
        | (3) Request to Grafana with JWT                            | (Optional:
        |                                                            |  JWT Validation Request)
        v                                                            |
+------------------+                                                 |
|    API Gateway   |--------------------------------------------------+
|  (e.g., Nginx,   |      (JWT Validation, Header Injection)
|  Envoy, APIPark) |
+------------------+
        |
        | (4) Authenticated Request with Grafana Headers
        v
+------------------+
|      Grafana     |
|   (Auth Proxy    |
|   Enabled)       |
+------------------+

Flow Breakdown:

  1. User Authentication: The user opens a web application (which could be a simple login page or part of a larger application) and enters their credentials. This request is sent to our Java Authentication Service.
  2. JWT Issuance: The Java Authentication Service verifies the credentials. If valid, it generates a signed JWT containing user claims (e.g., username, email, roles) and returns it to the client. The client stores this JWT (e.g., in an HttpOnly cookie or local storage).
  3. Grafana Request: When the user wants to access Grafana, their browser sends a request to the Grafana URL, including the stored JWT in the Authorization header.
  4. API Gateway Interception and Validation: This request first hits the API Gateway. The gateway is configured to:
    • Intercept all requests to Grafana.
    • Extract the JWT from the Authorization header.
    • Validate the JWT's signature, expiry, and other crucial claims (either internally or by calling a dedicated validation endpoint on the Java Authentication Service).
    • If the JWT is valid, the gateway then extracts relevant user information from the JWT's payload.
    • It injects this user information into specific HTTP headers (e.g., X-WEBAUTH-USER, X-WEBAUTH-EMAIL, X-Grafana-Org-Role), which Grafana is configured to trust.
    • The gateway then forwards this modified request to the Grafana instance.
  5. Grafana Access: Grafana receives the request with the trusted headers. It reads the user details from these headers, creates a user session (if one doesn't exist), and grants access according to the provided roles. Grafana itself doesn't validate the JWT; it relies entirely on the upstream gateway.

3.2 Components Involved

Let's detail the key architectural components:

  • Frontend Application (Login UI): This is the user-facing interface where users will initiate the login process. It could be a standalone SPA (Single Page Application) built with React, Angular, or Vue, or even a simple HTML form. Its primary role is to collect user credentials and send them to the Java Authentication Service, and then store the returned JWT securely.
  • Java Authentication Service:
    • Purpose: The central piece for user authentication and JWT management.
    • Functions: Handles user registration, login, password hashing, JWT generation (signing and embedding claims), and potentially JWT validation for other internal services or the API gateway.
    • Technologies: Spring Boot (for rapid development), Spring Security (for authentication/authorization), jjwt library (for JWT operations), a database (e.g., PostgreSQL, MySQL) for user storage.
  • API Gateway (e.g., Nginx, Envoy, or a specialized product like APIPark):
    • Purpose: Acts as the single entry point for all requests to Grafana (and potentially other backend services). It enforces security policies, handles routing, and centralizes JWT validation.
    • Functions:
      • Reverse Proxy: Forwards requests to Grafana.
      • JWT Validation: Intercepts requests, validates the incoming JWTs. This is a critical security boundary.
      • Header Injection: Adds Grafana-specific user headers based on the validated JWT claims.
      • Rate Limiting: Protects Grafana from abuse.
      • Load Balancing: Distributes traffic across multiple Grafana instances (if applicable).
      • Centralized Logging and Monitoring: Captures all incoming traffic and authentication attempts.
    • Importance: A robust API gateway significantly enhances the security posture by offloading authentication concerns from Grafana, providing a centralized point of control for all API traffic, and enabling advanced features like analytics and developer portals. For organizations dealing with a multitude of APIs, especially those incorporating AI models, a sophisticated API gateway like APIPark becomes indispensable. APIPark, an open-source AI gateway and API management platform, offers robust features for centralized API management, including unified authentication, rate limiting, and detailed logging. It can efficiently manage the flow of traffic, validate JWTs at the edge, and ensure secure access to applications like Grafana, providing an enterprise-grade solution that streamlines API integration and deployment.
  • Grafana Instance:
    • Purpose: The data visualization and monitoring platform.
    • Functions: Renders dashboards, connects to data sources, processes queries, and handles alerting.
    • Configuration: Must be configured to enable auth.proxy and trust the headers sent by the API gateway. It needs to know which headers to look for (X-WEBAUTH-USER, X-WEBAUTH-EMAIL, X-Grafana-Org-Role).

3.3 Security Considerations in the Design

Security must be interwoven throughout the design:

  • HTTPS Everywhere: All communication between the client, Java Authentication Service, API Gateway, and Grafana must use HTTPS/TLS to prevent eavesdropping and Man-in-the-Middle attacks. This protects credentials and JWTs in transit.
  • JWT Signing Key Management: The secret key used by the Java service to sign JWTs is paramount. It must be strong, kept strictly confidential, and stored securely (e.g., in environment variables, a secrets management system like HashiCorp Vault, or AWS Secrets Manager). Consider key rotation policies.
  • Secure Token Storage on Client: While local storage is often used for convenience, HttpOnly and Secure cookies are generally preferred for storing JWTs (or at least refresh tokens) to mitigate XSS attacks. The HttpOnly flag prevents client-side scripts from accessing the cookie, and the Secure flag ensures it's only sent over HTTPS.
  • CSRF and XSS Protection: The frontend application and Java service must implement robust protections against Cross-Site Request Forgery (CSRF) and Cross-Site Scripting (XSS) attacks. For example, ensuring proper input sanitization and utilizing CSRF tokens where appropriate.
  • Role-Based Access Control (RBAC): The JWT payload should include user roles. The API gateway (or the Java service during validation) will map these roles to Grafana's organizational roles. Grafana itself supports granular permissions for dashboards, folders, and data sources, allowing you to define who can see or edit what. Ensure this mapping is secure and prevents privilege escalation.
  • Input Validation: All input, especially user credentials and JWT claims, must be rigorously validated to prevent injection attacks and other vulnerabilities.
  • Logging and Auditing: Comprehensive logging of authentication attempts, token issuances, and validation failures on the Java service and the API gateway is essential for security auditing and incident response.
  • Rate Limiting: The API gateway should implement rate limiting on authentication endpoints and any publicly accessible APIs to prevent brute-force attacks and denial-of-service.
  • Error Handling: Ensure that error messages do not leak sensitive information. For example, a failed login attempt should not indicate whether the username or password was incorrect.

By carefully considering these architectural elements and security best practices, we can construct a robust and resilient system for securing Grafana with JWT and Java.

Section 4: Building the Java Authentication Service

The Java Authentication Service is the heart of our custom JWT solution. It's responsible for authenticating users and issuing the signed JWTs that will grant access to Grafana. We'll use Spring Boot for rapid development and jjwt for JWT operations.

4.1 Setting Up a Spring Boot Project

Let's start by setting up a basic Spring Boot project. We can use Spring Initializr (start.spring.io) with the following dependencies:

  • Spring Web: For building RESTful endpoints.
  • Spring Security: For user authentication and password encoding.
  • Spring Data JPA (Optional, but good for user storage): If you plan to store users in a database. For this guide, we'll start with in-memory users for simplicity.
  • H2 Database (Optional): An in-memory database if using JPA for local development.
  • Lombok (Optional): Reduces boilerplate code.
  • JJWT (JSON Web Token library): Manually add this dependency as it's not directly in Spring Initializr.

pom.xml additions for JJWT:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

(Note: Always use the latest stable versions of libraries.)

Basic Project Structure:

src/main/java/com/example/grafanasecurity
β”œβ”€β”€ GrafanaSecurityApplication.java
β”œβ”€β”€ config
β”‚   └── SecurityConfig.java
β”œβ”€β”€ controller
β”‚   └── AuthController.java
β”œβ”€β”€ model
β”‚   β”œβ”€β”€ AuthRequest.java
β”‚   └── AuthResponse.java
β”œβ”€β”€ service
β”‚   β”œβ”€β”€ CustomUserDetailsService.java
β”‚   └── JwtService.java
└── repository (optional, for DB users)
    └── UserRepository.java

4.2 User Management (Simplified for Demonstration)

For a real application, you'd use a database. Here, we'll keep it simple with an in-memory User object and UserDetails implementation.

// model/User.java
package com.example.grafanasecurity.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Collections;
import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String username;
    private String password; // Stored hashed in real app
    private String email;
    private List<String> roles; // e.g., "admin", "viewer", "editor"
}
// service/CustomUserDetailsService.java
package com.example.grafanasecurity.service;

import com.example.grafanasecurity.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    // In-memory user store for demonstration
    private final Map<String, User> users = new HashMap<>();
    private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @PostConstruct
    public void init() {
        // Hashing passwords for demonstration purposes
        users.put("admin", new User("admin", passwordEncoder.encode("adminpass"), "admin@example.com", List.of("admin", "editor")));
        users.put("viewer", new User("viewer", passwordEncoder.encode("viewerpass"), "viewer@example.com", List.of("viewer")));
        users.put("editor", new User("editor", passwordEncoder.encode("editorpass"), "editor@example.com", List.of("editor")));
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = users.get(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found with username: " + username);
        }
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                mapRolesToAuthorities(user.getRoles())
        );
    }

    public User getUserByUsername(String username) {
        return users.get(username);
    }

    private Collection<? extends GrantedAuthority> mapRolesToAuthorities(List<String> roles) {
        return roles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) // Spring Security convention
                .collect(Collectors.toList());
    }
}

4.3 JWT Generation Logic (JwtService.java)

This service handles the creation and validation of JWTs. The secret key should be loaded from environment variables or a configuration server.

// service/JwtService.java
package com.example.grafanasecurity.service;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

@Service
public class JwtService {

    @Value("${jwt.secret}")
    private String SECRET_KEY; // Keep this extremely secure!
    @Value("${jwt.expiration}")
    private long JWT_EXPIRATION_MS; // Token validity in milliseconds

    public String generateToken(UserDetails userDetails, List<String> roles) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", roles); // Custom claim for roles
        claims.put("email", ((com.example.grafanasecurity.model.User) userDetails).getEmail()); // Example: add email

        return createToken(claims, userDetails.getUsername());
    }

    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION_MS))
                .signWith(getSignKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSignKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    private Key getSignKey() {
        byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

application.properties for JWT configuration:

jwt.secret=YOUR_VERY_STRONG_AND_LONG_BASE64_ENCODED_SECRET_KEY
jwt.expiration=3600000 # 1 hour in milliseconds

Generate a secure base64 encoded secret key using java.util.Base64.getEncoder().encodeToString(Keys.secretKeyFor(SignatureAlgorithm.HS256).getEncoded()).

4.4 JWT Validation Logic (within JwtService.java)

The JwtService also contains the logic to validate a token. This is used internally by Spring Security filters and could be exposed via an API for the API gateway to consume.

4.5 Login Endpoint Development (AuthController.java)

This controller will expose the /login endpoint where users post their credentials.

// controller/AuthController.java
package com.example.grafanasecurity.controller;

import com.example.grafanasecurity.model.AuthRequest;
import com.example.grafanasecurity.model.AuthResponse;
import com.example.grafanasecurity.model.User;
import com.example.grafanasecurity.service.CustomUserDetailsService;
import com.example.grafanasecurity.service.JwtService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtService jwtService;
    private final CustomUserDetailsService userDetailsService;

    public AuthController(AuthenticationManager authenticationManager, JwtService jwtService, CustomUserDetailsService userDetailsService) {
        this.authenticationManager = authenticationManager;
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }

    @PostMapping("/login")
    public ResponseEntity<AuthResponse> authenticateAndGetToken(@RequestBody AuthRequest authRequest) {
        // Authenticate user with Spring Security
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
        );

        if (authentication.isAuthenticated()) {
            // If authenticated, generate JWT
            User user = userDetailsService.getUserByUsername(authRequest.getUsername());
            if (user == null) {
                 // Should not happen if authentication was successful
                 throw new RuntimeException("User details not found after successful authentication");
            }
            String token = jwtService.generateToken(userDetailsService.loadUserByUsername(authRequest.getUsername()), user.getRoles());
            return ResponseEntity.ok(new AuthResponse(token));
        } else {
            throw new RuntimeException("Invalid credentials!");
        }
    }

    // AuthRequest.java
    // model/AuthRequest.java
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class AuthRequest {
        private String username;
        private String password;
    }

    // AuthResponse.java
    // model/AuthResponse.java
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class AuthResponse {
        private String jwtToken;
    }
}

Spring Security Configuration (SecurityConfig.java): This configures Spring Security to use our CustomUserDetailsService and BCryptPasswordEncoder, and to expose the AuthenticationManager.

// config/SecurityConfig.java
package com.example.grafanasecurity.config;

import com.example.grafanasecurity.service.CustomUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomUserDetailsService userDetailsService;

    public SecurityConfig(CustomUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.csrf().disable()
                .authorizeHttpRequests()
                .requestMatchers("/auth/login").permitAll() // Allow login endpoint without authentication
                .anyRequest().authenticated() // All other requests require authentication
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Use stateless sessions for JWT
                .and()
                .authenticationProvider(authenticationProvider())
                .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        return authenticationProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

4.6 User Information Endpoint (for Grafana's auth_proxy_headers)

While our main authentication flow involves the API gateway validating the JWT and injecting headers, it's good practice to have an endpoint that, given a valid JWT, returns detailed user information. This can be useful for debugging or if the API gateway needs to dynamically fetch more details than what's in the JWT claims. However, for a direct auth.proxy integration, the API gateway usually extracts all necessary info directly from the JWT. Let's add a simple endpoint to demonstrate what a validated user looks like. This endpoint itself would be protected by a JWT filter. For simplicity, we omit the full JWT filter chain setup here as the primary focus is Grafana integration via the API gateway. The API gateway will perform the validation.

For the purpose of the API gateway needing to get user details after validating the token, one might consider an endpoint like /auth/userinfo. However, a more common pattern for auth.proxy is for the API gateway to directly parse the JWT and inject headers based on its content, without calling another backend endpoint. Our Java service's role is primarily to issue the token.

The core implementation for our Java service is now complete. It can authenticate users, generate JWTs with custom claims (like roles and email), and provide a base for validation.

Section 5: Configuring Grafana for JWT Authentication

With our Java Authentication Service ready to issue JWTs, the next crucial step is to configure Grafana to accept authenticated requests based on these tokens. As discussed, the auth.proxy mechanism is the most effective way to achieve this, allowing an upstream API gateway to handle the heavy lifting of JWT validation.

5.1 Choosing the Right Strategy: Auth Proxy

Grafana's auth.proxy configuration is designed for scenarios where an external system (like a reverse proxy or an API gateway) performs the actual user authentication. Once a user is authenticated by this external system, it injects specific HTTP headers into the request before forwarding it to Grafana. Grafana then trusts these headers as proof of authentication.

This approach offers significant benefits for our JWT integration:

  • Delegated Security: Grafana doesn't need to implement JWT parsing or validation logic, simplifying its configuration and reducing its security attack surface.
  • Centralized Control: The API gateway becomes the single point for enforcing authentication, authorization, rate limiting, and other security policies across multiple applications, including Grafana.
  • Flexibility: Allows for complex logic within the API gateway to map JWT claims to Grafana users, organizations, and roles.

Key auth.proxy Configuration Options

The [auth.proxy] section in your grafana.ini configuration file (or equivalent environment variables) is where you'll define how Grafana interacts with the upstream proxy.

  • enabled = true: This is the most critical setting, enabling the reverse proxy authentication method.
  • header_name = X-WEBAUTH-USER: Specifies the HTTP header that Grafana should look for to identify the authenticated user's login name. The API gateway must inject this header with the username from the JWT.
  • header_property = username: This determines which property on the Grafana user model the value of header_name maps to. username is the default and typically correct.
  • auto_sign_up = true: If set to true, Grafana will automatically create a new user account if a user with the specified header_name (username) does not already exist in its database. This simplifies user management, as you don't need to pre-provision users in Grafana.
  • sync_ttl = 60s: (Available in Grafana 9.x+) How often user attributes (like roles, email) are synchronized from the proxy headers.
  • whitelist =: An optional comma-separated list of IP addresses or CIDR ranges. If set, Grafana will only accept auth.proxy headers from these whitelisted IPs, significantly enhancing security by preventing spoofing from unauthorized sources. This is highly recommended in production.
  • headers = X-WEBAUTH-USER X-WEBAUTH-EMAIL X-Grafana-Org X-Grafana-Org-Id X-Grafana-Org-Role: This crucial setting defines additional headers Grafana should look for to extract user attributes beyond just the username.
    • X-WEBAUTH-EMAIL: For the user's email address.
    • X-Grafana-Org: To set the organization name for the user.
    • X-Grafana-Org-Id: To set the organization ID.
    • X-Grafana-Org-Role: Extremely important for authorization. This header tells Grafana what role the user should have within the specified organization (e.g., Viewer, Editor, Admin). The API gateway will derive this from the JWT's roles claim.

Grafana Configuration Precedence: Grafana configuration settings can be defined in grafana.ini, environment variables (e.g., GF_AUTH_PROXY_ENABLED=true), or command-line arguments. Environment variables are often preferred in containerized deployments.

5.2 Example grafana.ini Configuration for Auth Proxy

Here's an example of how you might configure your grafana.ini (or set environment variables) for auth.proxy integration.

[server]
# Your Grafana listen address and port
http_port = 3000
domain = grafana.example.com
root_url = https://grafana.example.com/ # Important for correct redirects and links

[auth.proxy]
enabled = true
header_name = X-WEBAUTH-USER  # Header for username
header_property = username
auto_sign_up = true           # Automatically create users if they don't exist
sync_ttl = 60s                # Synchronize user attributes every 60 seconds
whitelist = 127.0.0.1, 192.168.1.0/24 # IMPORTANT: Restrict proxy to specific IPs (replace with your API Gateway's IP)
# Example of additional headers for email, organization, and role
headers = X-WEBAUTH-EMAIL:email X-Grafana-Org:organization X-Grafana-Org-Id:org_id X-Grafana-Org-Role:role

# Map the headers to specific user attributes within Grafana
[auth.proxy.headers]
# These keys in the `headers` map to the values received from the proxy
# If you don't specify, Grafana will use the header name directly as the user attribute name.
# For example, X-WEBAUTH-EMAIL will map to the email field of the user object.
# The `headers` option above is a shorthand, for more complex mapping, you might list them out here.
# For example:
# Email = X-User-Email  (if your proxy sends X-User-Email instead of X-WEBAUTH-EMAIL)
# Name = X-User-Name
# Groups = X-User-Groups

Table: Key Grafana auth.proxy Configuration Settings

| grafana.ini Setting | Description | Example Value (grafana.ini or Env Var)

πŸš€You can securely and efficiently call the OpenAI API on APIPark in just two steps:

Step 1: Deploy the APIPark AI gateway in 5 minutes.

APIPark is developed based on Golang, offering strong product performance and low development and maintenance costs. You can deploy APIPark with a single command line.

curl -sSO https://download.apipark.com/install/quick-start.sh; bash quick-start.sh
APIPark Command Installation Process

In my experience, you can see the successful deployment interface within 5 to 10 minutes. Then, you can log in to APIPark using your account.

APIPark System Interface 01

Step 2: Call the OpenAI API.

APIPark System Interface 02
Article Summary Image