{"id":23884718,"url":"https://github.com/cungthinh/authentication_service","last_synced_at":"2026-06-17T08:31:22.823Z","repository":{"id":270863897,"uuid":"911251564","full_name":"CungThinh/authentication_service","owner":"CungThinh","description":null,"archived":false,"fork":false,"pushed_at":"2025-01-27T05:08:33.000Z","size":158,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-23T01:44:31.160Z","etag":null,"topics":["authentication","github-actions","jwt-auth","rbac","reactjs","session-auth","sonarqube","spring-boot","unit-testing"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/CungThinh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-01-02T15:30:27.000Z","updated_at":"2025-01-27T05:08:36.000Z","dependencies_parsed_at":null,"dependency_job_id":"dae04f98-61ff-4342-bef4-fcf0d34f3b93","html_url":"https://github.com/CungThinh/authentication_service","commit_stats":null,"previous_names":["cungthinh/authentication_service"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/CungThinh/authentication_service","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CungThinh%2Fauthentication_service","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CungThinh%2Fauthentication_service/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CungThinh%2Fauthentication_service/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CungThinh%2Fauthentication_service/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/CungThinh","download_url":"https://codeload.github.com/CungThinh/authentication_service/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CungThinh%2Fauthentication_service/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34441282,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-17T02:00:05.408Z","response_time":127,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","github-actions","jwt-auth","rbac","reactjs","session-auth","sonarqube","spring-boot","unit-testing"],"created_at":"2025-01-04T03:17:39.932Z","updated_at":"2026-06-17T08:31:22.818Z","avatar_url":"https://github.com/CungThinh.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# Auth Services\n\nAuthentication Services with Spring Boot for Backend and ReactJS for Frontend\n\n**Branch** `main`: JWT with nimbus-jose-jwt (oauth2-resource-server) \n\n**Branch** `jwt-with-iojsonwebtoken`: JWT with iojsonwebtoken - manual config. \n\n**Branch** `session-auth`: JWT with Spring Session and Redis. \n\n\n# Tính năng\n\n- Đăng ký, Đăng nhập\n- Xác thực bằng email và mật khẩu.\n- Kiểm soát quyền truy cập theo vai trò với Vai trò và Quyền hạn.\n- Xác thực JWT.\n- Blacklist token với Redis\n- Xác thực bằng Session.\n- Lưu trữ session trong Redis.\n- Chuẩn hóa ApiResponse và handle lỗi bằng GlobalExceptionHandler\n- Unit Test và Integration Test\n- Kiểm thử sử dụng TestContainer hoặc H2 Database cho môi trường cô lập.\n- SonarQube để duy trì chất lượng mã nguồn.\n- Deploy SonarQube lên EC2 để CICD với Github Actions.\n\n# JWT\n### Khởi tạo token\n```java\npublic String generateToken(UserEntity user) {\n\n        JWSHeader header = new JWSHeader(JWSAlgorithm.HS512);\n\n        JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder()\n                .subject(user.getId())\n                .issuer(\"cungthinh\")\n                .claim(\"email\", user.getEmail())\n                .claim(\"scope\", buildScope(user))\n                .issueTime(new Date())\n                .expirationTime(new Date(\n                        Instant.now().plus(expired_duration, ChronoUnit.SECONDS).toEpochMilli()))\n                .build();\n\n        Payload payload = new Payload(jwtClaimsSet.toJSONObject());\n\n        JWSObject jwsObject = new JWSObject(header, payload);\n\n        try {\n            jwsObject.sign(new MACSigner(secretKey.getBytes()));\n            return jwsObject.serialize();\n        } catch (JOSEException e) {\n            throw new RuntimeException(\"Khởi tạo token bị lỗi\");\n        }\n    }\n```\n### Xác thực token\n```java\npublic SignedJWT verifyToken(String token, boolean isRefresh) throws JOSEException, ParseException {\n        JWSVerifier verifier = new MACVerifier(secretKey.getBytes());\n        SignedJWT signedJWT = SignedJWT.parse(token);\n\n        Date expirationTime;\n        if (isRefresh) {\n            expirationTime = new Date(signedJWT\n                    .getJWTClaimsSet()\n                    .getIssueTime()\n                    .toInstant()\n                    .plus(refreshable_duration, ChronoUnit.SECONDS)\n                    .toEpochMilli());\n        } else {\n            expirationTime = signedJWT.getJWTClaimsSet().getExpirationTime();\n        }\n\n        if (!signedJWT.verify(verifier)) {\n            throw new CustomException(ErrorCode.INVALID_TOKEN);\n        }\n\n        if (expirationTime.before(new Date())) {\n            //            log.info(\"Token hết hạn\");\n            throw new CustomException(ErrorCode.UNAUTHENTICATED);\n        }\n\n        if (jwtBlackListService.isTokenBlacklisted(token)) {\n            throw new CustomException(ErrorCode.UNAUTHORIZED);\n        }\n\n        return signedJWT;\n    }\n```\n\n### Integration Test\n```java\n...\npublic class UserControllerIntegrationTest {\n    ...\n\n    @BeforeEach\n    void initData() {\n        UserEntity testUser = UserEntity.builder()\n                .email(\"test@example.com\")\n                .password(\"password123\")\n                .build();\n\n        savedUser = userResipotory.saveAndFlush(testUser);\n    }\n...\n    @Test\n    @WithMockUser(\n            username = \"test-uuid-123\",\n            roles = {\"USER\"})\n    void getMyInfo_validRequest_success() throws Exception {\n        // Now use @WithMockUser with the generated ID\n        mockMvc.perform(MockMvcRequestBuilders.get(\"/api/v1/users/me\")\n                        .with(user(savedUser.getId()).roles(\"USER\")))\n                .andExpect(MockMvcResultMatchers.status().isOk());\n    }\n}\n```\n\n# Session\n```java\nprivate final RedisIndexedSessionRepository redisIndexedSessionRepository;\n\n    public SecurityConfig(RedisIndexedSessionRepository redisIndexedSessionRepository) {\n        this.redisIndexedSessionRepository = redisIndexedSessionRepository;\n    }\n```\n\n```java\n.sessionManagement(sessionManagement -\u003e sessionManagement\n                        .sessionCreationPolicy(IF_REQUIRED) //\n                        .sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::newSession) //\n                        .maximumSessions(1) //\n                        .sessionRegistry(sessionRegistry()));\n```\n\n### Sử dụng SpringSessionBackedSessionRegistry để lưu trữ Session\n```java\n@Bean\n    public SpringSessionBackedSessionRegistry\u003c? extends Session\u003e sessionRegistry() {\n        return new SpringSessionBackedSessionRegistry\u003c\u003e(this.redisIndexedSessionRepository);\n    }\n\n```\n### Sử dụng Test Container\n```java\n@Container\n    static final PostgreSQLContainer\u003c?\u003e postgreSQLContainer = new PostgreSQLContainer\u003c\u003e(\"postgres:latest\")\n            .withDatabaseName(\"authservices\")\n            .withUsername(\"postgres\")\n            .withPassword(\"dontwastetime\");\n\n    @Container\n    private static final RedisContainer redis =\n            new RedisContainer(DockerImageName.parse(\"redis:alpine\")).withExposedPorts(6379);\n\n    @DynamicPropertySource\n    public static void properties(DynamicPropertyRegistry registry) {\n        // primary\n        registry.add(\"spring.datasource.url\", postgreSQLContainer::getJdbcUrl);\n        registry.add(\"spring.datasource.username\", postgreSQLContainer::getUsername);\n        registry.add(\"spring.datasource.password\", postgreSQLContainer::getPassword);\n        registry.add(\"spring.jpa.hibernate.ddl-auto\", () -\u003e \"update\");\n        registry.add(\"spring.redis.host\", redis::getHost);\n        registry.add(\"spring.redis.port\", () -\u003e redis.getMappedPort(6379));\n    }\n```\n\n### Test session có bị invalidate không\n```java\n@Test\n    void loginOnSecondLoginThenFirstSessionTerminated() throws Exception {\n        // Set max session to 1\n        MvcResult mvcResult = this.mvc\n                .perform(post(\"/api/v1/auth/login\")\n                        .contentType(MediaType.APPLICATION_JSON)\n                        .content(\"{\\\"email\\\":\\\"macthin27@gmail.com\\\", \\\"password\\\":\\\"dontwastetime\\\"}\"))\n                .andExpect(status().isOk())\n                .andReturn();\n\n        String sessionCookie = mvcResult.getResponse().getCookie(\"JSESSIONID\").getValue();\n\n        this.mvc\n                .perform(get(\"/api/v1/auth/session-info\").cookie(new Cookie(\"JSESSIONID\", sessionCookie)))\n                .andExpect(status().isOk());\n\n        log.info(\"Second login\");\n        this.mvc\n                .perform(post(\"/api/v1/auth/login\")\n                        .contentType(MediaType.APPLICATION_JSON)\n                        .content(\"{\\\"email\\\":\\\"macthin27@gmail.com\\\", \\\"password\\\":\\\"dontwastetime\\\"}\"))\n                .andExpect(status().isOk());\n\n        this.mvc\n                .perform(get(\"/api/v1/auth/session-info\").cookie(new Cookie(\"JSESSIONID\", sessionCookie)))\n                .andExpect(status().isForbidden()); // Session terminated\n    }\n```\n\n# CICD với Github Actions\n``` yaml\nname: CI/CD workflow for Maven Build and Sonar Code scan\non:\n  push:\n    branches:\n      - main\n    paths:\n      - 'backend/**'  # Chỉ chạy khi có thay đổi trong thư mục backend\n  workflow_dispatch:\n\njobs:\n\n  build:\n    runs-on: ubuntu-latest\n    services:\n      postgres:\n        image: postgres:latest\n        ports:\n          - 5432:5432\n        env:\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: dontwastetime\n          POSTGRES_DB: authservices\n        options: \u003e-\n          --health-cmd=\"pg_isready -U postgres\"\n          --health-interval=10s\n          --health-timeout=5s\n          --health-retries=5\n          --health-start-period=30s\n    steps:\n    - name: Checkout code\n      uses: actions/checkout@v2\n    - name: Set up JDK 21\n      uses: actions/setup-java@v2\n      with:\n        distribution: 'adopt'\n        java-version: '21'\n    - name: Build with Maven\n      run: mvn clean install -X -f backend/authservices/pom.xml\n    - name: SonarQube Scan\n      uses: sonarsource/sonarqube-scan-action@master\n      with:\n        projectBaseDir: backend/authservices \n        args: \u003e\n          -Dsonar.organization=my-org\n          -Dsonar.projectKey=my-Java-web-app\n          -Dsonar.java.binaries=target/classes\n      env:\n        SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}\n        SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}\n```\n\n### Deploy SonarQube lên EC2\n``` yaml\nversion: '3'\n\nservices:\n  sonarqube:\n    image: sonarqube:community\n    container_name: sonarqube\n    ports:\n      - \"9000:9000\"\n    environment:\n      - sonar.jdbc.username=sonar\n      - sonar.jdbc.password=sonar\n      - sonar.jdbc.url=jdbc:postgresql://db:5432/sonar\n    depends_on:\n      - db\n    restart: always\n\n  db:\n    image: postgres:13\n    container_name: postgres\n    environment:\n      POSTGRES_USER: sonar\n      POSTGRES_PASSWORD: sonar\n      POSTGRES_DB: sonar\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n    restart: always\n\nvolumes:\n  postgres_data:\n```\n# Dependencies \u0026 Plugins\n- **spring-boot-starter-oauth2-resource-server**\n- **spring-boot-starter-data-jpa**\n- **com.nimbusds:nimbus-jose-jwt**\n- **spring-session-data-redis**\n- **spring-boot-starter-data-redis**\n- **io.jsonwebtoken**\n- **org.testcontainers:junit-jupiter**\n- **com.h2database:h2**\n- **com.fasterxml.jackson.datatype:jackson-datatype-jsr310**\n- **org.mapstruct:mapstruct**\n- **jacoco-maven-plugin**\n- **spotless-maven-plugin**\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcungthinh%2Fauthentication_service","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcungthinh%2Fauthentication_service","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcungthinh%2Fauthentication_service/lists"}