├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CMakeLists.txt ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples ├── CMakeLists.txt └── google_auth │ ├── CMakeLists.txt │ ├── README.md │ └── main.cpp ├── include └── minioauth2.hpp ├── test ├── CMakeLists.txt ├── main.cpp ├── minioauth2_test.cpp └── test_utils.cpp └── vcpkg.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: C++ CI 2 | 3 | on: 4 | push: 5 | branches: [ main, master, develop ] 6 | pull_request: 7 | branches: [ main, master, develop ] 8 | 9 | jobs: 10 | build_and_test: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ubuntu-latest, windows-latest] 15 | compiler: [gcc, clang, msvc] 16 | include: 17 | - os: windows-latest 18 | compiler: msvc 19 | cmake_generator: Visual Studio 17 2022 20 | exclude: 21 | - os: windows-latest 22 | compiler: gcc 23 | - os: windows-latest 24 | compiler: clang 25 | - os: ubuntu-latest 26 | compiler: msvc 27 | 28 | runs-on: ${{ matrix.os }} 29 | 30 | steps: 31 | - name: Checkout project code 32 | uses: actions/checkout@v4 33 | with: 34 | submodules: 'recursive' 35 | path: main_project 36 | 37 | # Linux Compiler Setup 38 | - name: Set up GCC (Linux) 39 | if: runner.os == 'Linux' && matrix.compiler == 'gcc' 40 | working-directory: main_project 41 | run: | 42 | echo ">>> LINUX GCC SETUP INITIATED (OS: ${{ runner.os }}, Compiler: ${{ matrix.compiler }}) <<<" 43 | echo "DEBUG: Setting up Linux compiler: gcc" 44 | sudo apt-get update --fix-missing 45 | sudo apt-get install -y gcc g++ ninja-build cmake 46 | echo "CC_FOR_CMAKE=gcc" >> $GITHUB_ENV 47 | echo "CXX_FOR_CMAKE=g++" >> $GITHUB_ENV 48 | 49 | - name: Set up Clang (Linux) 50 | if: runner.os == 'Linux' && matrix.compiler == 'clang' 51 | working-directory: main_project 52 | run: | 53 | echo ">>> LINUX CLANG SETUP INITIATED (OS: ${{ runner.os }}, Compiler: ${{ matrix.compiler }}) <<<" 54 | echo "DEBUG: Setting up Linux compiler: clang" 55 | sudo apt-get update --fix-missing 56 | sudo apt-get install -y clang ninja-build cmake 57 | echo "CC_FOR_CMAKE=clang" >> $GITHUB_ENV 58 | echo "CXX_FOR_CMAKE=clang++" >> $GITHUB_ENV 59 | 60 | # Windows MSVC Setup 61 | - name: Set up MSVC (Windows) 62 | if: runner.os == 'Windows' && matrix.compiler == 'msvc' 63 | uses: microsoft/setup-msbuild@v1.3 64 | 65 | - name: DEBUG - After MSVC Setup (Windows) 66 | if: runner.os == 'Windows' && matrix.compiler == 'msvc' 67 | run: | 68 | echo "DEBUG: MSVC setup step completed." 69 | 70 | # Windows vcpkg setup 71 | - name: Checkout vcpkg (Windows) 72 | if: runner.os == 'Windows' && matrix.compiler == 'msvc' 73 | uses: actions/checkout@v4 74 | with: 75 | repository: 'microsoft/vcpkg' 76 | ref: '2023.10.19' 77 | path: vcpkg 78 | 79 | - name: Bootstrap vcpkg (Windows) 80 | if: runner.os == 'Windows' && matrix.compiler == 'msvc' 81 | working-directory: vcpkg 82 | run: .\bootstrap-vcpkg.bat -disableMetrics 83 | shell: cmd 84 | 85 | - name: Install dependencies with vcpkg (Windows) 86 | if: runner.os == 'Windows' && matrix.compiler == 'msvc' 87 | working-directory: ${{ github.workspace }}/main_project # vcpkg.json is here 88 | shell: pwsh 89 | run: | 90 | echo "DEBUG: Current directory for vcpkg install: $(Get-Location)" 91 | echo "DEBUG: Attempting to install dependencies using vcpkg.json from $(Get-Location)" 92 | echo "DEBUG: vcpkg.exe path to be used: ${{ github.workspace }}\vcpkg\vcpkg.exe" 93 | echo "DEBUG: --vcpkg-root path to be used: ${{ github.workspace }}\vcpkg" 94 | & "${{ github.workspace }}\vcpkg\vcpkg.exe" install --triplet x64-windows-static --vcpkg-root "${{ github.workspace }}\vcpkg" 95 | echo "DEBUG: vcpkg install command finished." 96 | 97 | - name: DEBUG - After vcpkg Setup (Windows) 98 | if: runner.os == 'Windows' && matrix.compiler == 'msvc' 99 | run: | 100 | echo "DEBUG: vcpkg setup and install step completed." 101 | echo "DEBUG: VCPKG_ROOT (expected) is ${{ github.workspace }}\vcpkg" 102 | 103 | # CMake Configuration 104 | - name: Configure CMake (Windows) 105 | if: runner.os == 'Windows' && matrix.compiler == 'msvc' 106 | working-directory: main_project 107 | shell: cmd 108 | run: | 109 | echo "DEBUG: Starting CMake configuration for Windows (MSVC)." 110 | cmake -B build -S . -G "${{ matrix.cmake_generator }}" -DMINIOAUTH2_BUILD_EXAMPLES=ON -DMINIOAUTH2_BUILD_TESTS=ON -DMINIOAUTH2_USE_NLOHMANN_JSON=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE="%GITHUB_WORKSPACE%\vcpkg\scripts\buildsystems\vcpkg.cmake" 111 | echo "DEBUG: CMake configuration for Windows (MSVC) finished." 112 | 113 | - name: Configure CMake (Linux) 114 | if: runner.os == 'Linux' && (matrix.compiler == 'gcc' || matrix.compiler == 'clang') 115 | working-directory: main_project 116 | env: 117 | CC: ${{ env.CC_FOR_CMAKE }} 118 | CXX: ${{ env.CXX_FOR_CMAKE }} 119 | run: | 120 | echo "DEBUG: Starting CMake configuration for Linux (${{ matrix.compiler }})." 121 | echo "DEBUG: CC is $CC, CXX is $CXX" 122 | if [ -z "$CC" ] || [ -z "$CXX" ]; then 123 | echo "::error:: CC or CXX environment variables are not set for Linux CMake configuration ($CC, $CXX)." 124 | exit 1 125 | fi 126 | cmake -B build -S . -G Ninja -DMINIOAUTH2_BUILD_EXAMPLES=ON -DMINIOAUTH2_BUILD_TESTS=ON -DMINIOAUTH2_USE_NLOHMANN_JSON=ON -DCMAKE_BUILD_TYPE=Debug -D CMAKE_C_COMPILER=$CC -D CMAKE_CXX_COMPILER=$CXX 127 | echo "DEBUG: CMake configuration for Linux (${{ matrix.compiler }}) finished." 128 | 129 | # Build and Test 130 | - name: Build 131 | working-directory: main_project 132 | run: cmake --build build --config Debug 133 | 134 | - name: Run tests 135 | if: runner.os != 'Windows' 136 | working-directory: main_project/build 137 | run: ctest -C Debug --output-on-failure 138 | 139 | # TODO: Add steps for: 140 | # - Static analysis (clang-tidy) 141 | # run: | 142 | # sudo apt-get install clang-tidy 143 | # cmake -B build-tidy -S . -DCMAKE_CXX_CLANG_TIDY="clang-tidy;-checks=*;" 144 | # cmake --build build-tidy 145 | # - Sanitizer builds (ASan, UBSan) 146 | # run: | 147 | # cmake -B build-asan -S . -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-fsanitize=address -g" 148 | # cmake --build build-asan 149 | # # Run tests under ASan 150 | # - Check for Boost usage (and fail if found) 151 | # run: | 152 | # grep -r -E 'boost::|BOOST_' include/ examples/ test/ 153 | # if [ $? == 0 ]; then exit 1; fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # CMake / Build directories 2 | [Bb]uild/ 3 | [Bb]uild*/ 4 | [Dd]ebug/ 5 | [Rr]elease/ 6 | *.out 7 | *.exe 8 | *.dll 9 | *.lib 10 | *.exp 11 | *.pdb 12 | *.ilk 13 | 14 | # Fetched Dependencies (CMake FetchContent / vcpkg) 15 | _deps/ 16 | vcpkg_installed/ 17 | 18 | # IDE specific files 19 | .vscode/ 20 | .vs/ 21 | *.VC.db 22 | *.suo 23 | *.user 24 | 25 | # OS specific files 26 | .DS_Store 27 | Thumbs.db 28 | 29 | # CMake build files 30 | CMakeCache.txt 31 | CMakeFiles/ 32 | CMakeScripts/ 33 | Testing/ 34 | Makefile 35 | cmake_install.cmake 36 | install_manifest.txt 37 | compile_commands.json 38 | CTestTestfile.cmake 39 | 40 | # Build directories 41 | Build/ 42 | bin/ 43 | lib/ 44 | 45 | # Visual Studio files 46 | *.sln 47 | *.vcxproj 48 | *.vcxproj.filters 49 | *.vcxproj.user 50 | *.suo 51 | *.sdf 52 | *.opensdf 53 | *.db 54 | *.ipch 55 | *.aps 56 | *.ncb 57 | *.VC.db 58 | */x64/ 59 | */ARM/ 60 | */ARM64/ 61 | */Win32/ 62 | *.[Pp]db 63 | *.[Ii]db 64 | *.[Oo]bj 65 | *.[Ll]og 66 | *.[Tt]log 67 | 68 | # Qt files 69 | *.pro.user 70 | *.qbs.user 71 | 72 | # Other 73 | *.swp 74 | *~ 75 | 76 | # Specific to this project example temp storage 77 | /examples/google_auth/session_store.tmp # If we add file-based session later -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | project(MiniOAuth2 LANGUAGES CXX VERSION 0.1.0) 4 | 5 | # --- Options --- 6 | # Option for C++ Standard (default to 20, allow 17) 7 | set(MINIOAUTH2_CXX_STANDARD 20 CACHE STRING "C++ standard to use (e.g., 17, 20)") 8 | set_property(CACHE MINIOAUTH2_CXX_STANDARD PROPERTY STRINGS 17 20) 9 | if (NOT MINIOAUTH2_CXX_STANDARD VERSION_EQUAL 17 AND NOT MINIOAUTH2_CXX_STANDARD VERSION_EQUAL 20) 10 | message(FATAL_ERROR "Unsupported C++ standard: ${MINIOAUTH2_CXX_STANDARD}. Please use 17 or 20.") 11 | endif() 12 | set(CMAKE_CXX_STANDARD ${MINIOAUTH2_CXX_STANDARD}) 13 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 14 | set(CMAKE_CXX_EXTENSIONS OFF) 15 | 16 | # --- Options --- 17 | option(MINIOAUTH2_USE_NLOHMANN_JSON "Enable nlohmann/json for token parsing" ON) 18 | 19 | # Options for building tests and examples (default OFF when used as subproject) 20 | if(${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME}) 21 | # Top-level project build defaults 22 | set(MINIOAUTH2_BUILD_DEFAULT ON) 23 | else() 24 | # Subproject build defaults 25 | set(MINIOAUTH2_BUILD_DEFAULT OFF) 26 | endif() 27 | option(MINIOAUTH2_BUILD_EXAMPLES "Build examples" ${MINIOAUTH2_BUILD_DEFAULT}) 28 | option(MINIOAUTH2_BUILD_TESTS "Build tests" ${MINIOAUTH2_BUILD_DEFAULT}) 29 | 30 | # --- Dependencies --- 31 | include(FetchContent) 32 | 33 | if(MINIOAUTH2_USE_NLOHMANN_JSON) 34 | FetchContent_Declare( 35 | nlohmann_json 36 | GIT_REPOSITORY https://github.com/nlohmann/json.git 37 | GIT_TAG v3.11.3 # Or latest stable tag 38 | ) 39 | FetchContent_MakeAvailable(nlohmann_json) 40 | endif() 41 | 42 | # --- Add GoogleTest Dependency --- 43 | enable_testing() 44 | 45 | FetchContent_Declare( 46 | googletest 47 | GIT_REPOSITORY https://github.com/google/googletest.git 48 | GIT_TAG v1.14.0 # Or release-1.14.0 or main 49 | ) 50 | # For Windows: Prevent overriding the parent project's compiler/linker settings 51 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) 52 | FetchContent_MakeAvailable(googletest) 53 | message(STATUS "GoogleTest enabled for testing.") 54 | 55 | # --- Library Target --- 56 | add_library(minioauth2 INTERFACE) 57 | target_include_directories(minioauth2 INTERFACE $ $) 58 | target_compile_features(minioauth2 INTERFACE cxx_std_${CMAKE_CXX_STANDARD}) 59 | 60 | if(MINIOAUTH2_USE_NLOHMANN_JSON AND TARGET nlohmann_json::nlohmann_json) 61 | target_link_libraries(minioauth2 INTERFACE nlohmann_json::nlohmann_json) 62 | target_compile_definitions(minioauth2 INTERFACE MINIOAUTH2_USE_NLOHMANN_JSON) 63 | endif() 64 | 65 | # --- Installation --- 66 | # For a header-only library, just install the include directory. 67 | include(GNUInstallDirs) 68 | install(DIRECTORY include/ 69 | DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} 70 | COMPONENT Devel # Optional component name 71 | ) 72 | 73 | # --- Subdirectories --- 74 | if(MINIOAUTH2_BUILD_EXAMPLES) 75 | add_subdirectory(examples) 76 | endif() 77 | 78 | if(MINIOAUTH2_BUILD_TESTS) 79 | add_subdirectory(test) 80 | endif() -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to MiniOAuth2 2 | 3 | First off, thank you for considering contributing to MiniOAuth2! We appreciate your time and effort. 4 | 5 | This document provides guidelines for contributing to the project. 6 | 7 | ## How Can I Contribute? 8 | 9 | ### Reporting Bugs 10 | 11 | * Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/Mhr1375/MiniOAuth2/issues). 12 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/Mhr1375/MiniOAuth2/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. 13 | 14 | ### Suggesting Enhancements 15 | 16 | * Open an issue and provide a clear description of the suggested enhancement and its potential benefits. 17 | * Explain why this enhancement would be useful to most MiniOAuth2 users. 18 | * Provide code examples if possible to illustrate the use case. 19 | 20 | ### Pull Requests 21 | 22 | * Fork the repository and create your branch from `master`. 23 | * If you've added code that should be tested, add tests. 24 | * Ensure the test suite passes (`ctest` in the build directory). 25 | * Make sure your code lints (if a linter is set up). 26 | * Issue that pull request! 27 | 28 | ## Areas for Contribution 29 | 30 | We are actively looking for contributions in the following areas: 31 | 32 | * **More Unit Tests:** Expanding test coverage for existing and new functionality is always welcome. Especially for edge cases in parsing, encoding, and PKCE generation. 33 | * **Additional OAuth Providers:** Adding predefined configurations (`minioauth2::config::ProviderName()`) for other popular OAuth 2.0 providers (e.g., GitHub, Microsoft, Facebook). 34 | * **Token Refresh Flow:** Implementing helper functions for the token refresh grant type. 35 | * **JWT Validation:** While full JWT validation is complex, adding basic claim validation (e.g., `exp`, `aud`, `iss`) could be a valuable optional feature (perhaps requiring another dependency). 36 | * **Improved Error Handling:** Making error messages more specific and potentially introducing custom exception types. 37 | * **Example Enhancements:** Improving the existing example or adding new ones (e.g., showing refresh token usage, using a different web framework). 38 | * **Documentation:** Improving comments in the code or enhancing the README. 39 | 40 | ## Styleguides 41 | 42 | * Try to follow the existing code style (consistent indentation, naming conventions, etc.). 43 | * Use comments where necessary to explain complex logic. 44 | 45 | ## Questions? 46 | 47 | Feel free to open an issue if you have questions about contributing or the project in general. 48 | 49 | We look forward to your contributions! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Your Name / Your Organization 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MiniOAuth2 2 | 3 | A header-only C++20 library for simplifying the OAuth 2.0 Authorization Code Flow with PKCE, designed with [CrowCpp](https://github.com/CrowCpp/Crow) in mind, but usable independently. 4 | 5 | Author: [Mhr1375](https://github.com/Mhr1375) 6 | 7 | ## Features 8 | 9 | * **Header-Only:** Easy integration, just include `minioauth2.hpp`. 10 | * **C++20:** Uses modern C++ features. 11 | * **PKCE Support:** Implements Proof Key for Code Exchange (RFC 7636) using SHA-256 (`S256` method) for enhanced security, especially for public clients (like native apps or SPAs). 12 | * **Helper Utilities:** Includes functions for generating secure random strings (`state`, `code_verifier`), URL-safe Base64 encoding/decoding, URL encoding/decoding, SHA-256 hashing (via embedded PicoSHA2), and building authorization/token request parameters. 13 | * **Optional JSON Parsing:** Can use `nlohmann/json` (if `MINIOAUTH2_USE_NLOHMANN_JSON` is defined via CMake) to parse token responses and JWT payloads (ID tokens). **Note:** JWT parsing does *not* validate signatures or claims. 14 | * **Crow Example:** Includes an example (`examples/google_auth`) demonstrating usage with CrowCpp for a basic Google login flow. 15 | 16 | ## Dependencies 17 | 18 | * **Core Library:** Requires a C++20 compliant compiler. Optionally uses `nlohmann/json` (fetched via CMake `FetchContent`). 19 | * **Google Auth Example:** 20 | * [CrowCpp](https://github.com/CrowCpp/Crow) (fetched via CMake `FetchContent`). 21 | * [cpp-httplib](https://github.com/yhirose/cpp-httplib) (fetched via CMake `FetchContent`) for making HTTP requests. 22 | * **OpenSSL:** Required by `cpp-httplib` for HTTPS communication. Must be installed separately on the system and findable by CMake. 23 | 24 | ## Building 25 | 26 | This project uses CMake for building the library (as an INTERFACE target) and the example. 27 | 28 | ### Prerequisites 29 | 30 | 1. **C++20 Compiler:** (e.g., GCC 10+, Clang 10+, MSVC v19.28+). 31 | 2. **CMake:** Version 3.15 or higher. 32 | 3. **Git:** For cloning and fetching dependencies. 33 | 4. **(For Example)** **OpenSSL Development Libraries:** 34 | * **Linux (apt):** `sudo apt-get update && sudo apt-get install libssl-dev` 35 | * **macOS (brew):** `brew install openssl` (CMake might need hints like `-DOPENSSL_ROOT_DIR=$(brew --prefix openssl)`) 36 | * **Windows:** 37 | * **Recommended: `vcpkg`** 38 | 1. Install [vcpkg](https://vcpkg.io/en/getting-started.html). 39 | 2. Install OpenSSL: `vcpkg install openssl:x64-windows` (or your target triplet). 40 | 3. Configure CMake with the vcpkg toolchain file: 41 | ```bash 42 | cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=[path/to/vcpkg]/scripts/buildsystems/vcpkg.cmake 43 | ``` 44 | * **Manual Installation:** 45 | 1. Download and install pre-compiled binaries (including **development headers and libraries**) from a trusted source (e.g., [Shining Light Productions](https://slproweb.com/products/Win32OpenSSL.html)). Make sure to install the version matching your target architecture (e.g., Win64). 46 | 2. Configure CMake, telling it where to find OpenSSL: 47 | ```bash 48 | cmake -B build -S . -DOPENSSL_ROOT_DIR="C:/path/to/OpenSSL-Win64" 49 | ``` 50 | (Replace the path with your actual installation directory). CMake should automatically find the includes and libraries within this root directory if the installation layout is standard. 51 | 52 | ### Build Steps 53 | 54 | 1. **Clone the repository:** 55 | ```bash 56 | git clone https://github.com/Mhr1375/MiniOAuth2.git # Or your fork's URL 57 | cd MiniOAuth2 58 | ``` 59 | 2. **Configure CMake:** (Choose ONE of the Windows methods if applicable) 60 | ```bash 61 | # Standard (Linux/macOS with OpenSSL installed system-wide or via brew) 62 | cmake -B build -S . -DMINIOAUTH2_BUILD_EXAMPLE=ON -DMINIOAUTH2_USE_NLOHMANN_JSON=ON 63 | 64 | # Windows with vcpkg 65 | # cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=[path/to/vcpkg]/scripts/buildsystems/vcpkg.cmake -DMINIOAUTH2_BUILD_EXAMPLE=ON -DMINIOAUTH2_USE_NLOHMANN_JSON=ON 66 | 67 | # Windows with manual OpenSSL install 68 | # cmake -B build -S . -DOPENSSL_ROOT_DIR="C:/path/to/OpenSSL-Win64" -DMINIOAUTH2_BUILD_EXAMPLE=ON -DMINIOAUTH2_USE_NLOHMANN_JSON=ON 69 | ``` 70 | * `-DMINIOAUTH2_BUILD_EXAMPLE=ON`: Builds the `google_auth_example`. (Default is ON) 71 | * `-DMINIOAUTH2_USE_NLOHMANN_JSON=ON`: Enables `nlohmann/json` support. (Default is ON) 72 | 73 | 3. **Build:** 74 | ```bash 75 | cmake --build build 76 | ``` 77 | * On Windows with Visual Studio, you might need to specify the configuration: `cmake --build build --config Release` (or `Debug`). 78 | 79 | ## Running the Google Auth Example 80 | 81 | 1. **Set up Google Cloud Credentials:** 82 | * Go to the [Google Cloud Console](https://console.cloud.google.com/). 83 | * Create a new project or select an existing one. 84 | * Go to "APIs & Services" -> "Credentials". 85 | * Create new "OAuth client ID" credentials. 86 | * Choose "Web application" as the application type. 87 | * Add an "Authorized redirect URI": `http://localhost:18080/callback` (This must match the `redirect_uri` used in the code). 88 | * Note down your "Client ID" and "Client Secret". 89 | 90 | 2. **Set Environment Variables:** Before running the example, set the following environment variables in your terminal: 91 | * `GOOGLE_CLIENT_ID`: Your Google Client ID. 92 | * `GOOGLE_CLIENT_SECRET`: Your Google Client Secret. 93 | * `GOOGLE_REDIRECT_URI`: The redirect URI you configured (`http://localhost:18080/callback`). 94 | * **Example (PowerShell):** 95 | ```powershell 96 | $env:GOOGLE_CLIENT_ID="YOUR_ID.apps.googleusercontent.com" 97 | $env:GOOGLE_CLIENT_SECRET="YOUR_SECRET" 98 | $env:GOOGLE_REDIRECT_URI="http://localhost:18080/callback" 99 | ``` 100 | * **Example (Bash/Zsh):** 101 | ```bash 102 | export GOOGLE_CLIENT_ID="YOUR_ID.apps.googleusercontent.com" 103 | export GOOGLE_CLIENT_SECRET="YOUR_SECRET" 104 | export GOOGLE_REDIRECT_URI="http://localhost:18080/callback" 105 | ``` 106 | 107 | 3. **Run the Executable:** 108 | * Navigate to the project root directory in your terminal. 109 | * Run the compiled example: 110 | * **Windows:** `.\\build\\examples\\google_auth\\Debug\\google_auth_example.exe` (or `Release` if built with that config) 111 | * **Linux/macOS:** `./build/examples/google_auth/google_auth_example` 112 | 113 | 4. **Test the Flow:** 114 | * The terminal will show "INFO: Server is running on port 18080". 115 | * Open your web browser and go to `http://localhost:18080/login`. 116 | * You should be redirected to the Google login page. 117 | * Log in and grant the requested permissions (openid, profile, email). 118 | * You will be redirected back to `http://localhost:18080/callback`. 119 | * Check the terminal where the example is running. It should print the received access token, token type, ID token payload, etc. 120 | 121 | **Note:** The example uses a simple in-memory `std::map` to store the `state` and `code_verifier` between the `/login` and `/callback` requests. This is **not suitable for production** as it's insecure and doesn't handle multiple users or server restarts. A real application would need a proper session management mechanism (e.g., encrypted cookies, server-side session store). 122 | 123 | ## Running Tests 124 | 125 | Unit tests are implemented using GoogleTest (fetched via CMake `FetchContent`). 126 | 127 | 1. Ensure the project is configured with testing enabled (default CMake option `MINIOAUTH2_ENABLE_TESTING=ON`). 128 | 2. Build the project as described above. This will also build the `minioauth2_tests` executable. 129 | 3. Run tests using CTest from the build directory: 130 | ```bash 131 | cd build 132 | ctest # Or ctest -C Debug on Windows/Multi-config generators 133 | ``` 134 | 135 | ## License 136 | 137 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 138 | 139 | It includes PicoSHA2, which is also distributed under the MIT License. -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Add the google_auth example subdirectory 2 | add_subdirectory(google_auth) -------------------------------------------------------------------------------- /examples/google_auth/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | project(GoogleAuthExample CXX) 4 | 5 | set(CMAKE_CXX_STANDARD 20) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | set(CMAKE_CXX_EXTENSIONS OFF) 8 | 9 | # --- Dependencies --- 10 | # No need to find_package for MiniOAuth2 when building together 11 | # find_package(MiniOAuth2 REQUIRED) 12 | 13 | include(FetchContent) 14 | 15 | # Fetch Asio (standalone) dependency FIRST 16 | FetchContent_Declare( 17 | asio 18 | GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git 19 | GIT_TAG asio-1-28-1 # Use a recent stable tag 20 | GIT_SHALLOW TRUE 21 | SOURCE_SUBDIR asio/include # Asio headers are in asio/include within the repo 22 | ) 23 | FetchContent_MakeAvailable(asio) 24 | 25 | # Explicitly set ASIO_INCLUDE_DIR for Crow to find 26 | # Use the known path where FetchContent places the source (relative to top-level build dir) 27 | set(ASIO_INCLUDE_DIR ${CMAKE_BINARY_DIR}/_deps/asio-src/asio/include CACHE PATH "Path to Asio include directory" FORCE) 28 | message(STATUS "ASIO_INCLUDE_DIR set to: ${ASIO_INCLUDE_DIR}") # For debugging 29 | 30 | # Fetch Crow dependency 31 | FetchContent_Declare( 32 | Crow 33 | GIT_REPOSITORY https://github.com/CrowCpp/Crow.git 34 | GIT_TAG v1.1.0 # Or latest stable tag 35 | GIT_SHALLOW TRUE 36 | # Remove AMALGAMATION, keep other options 37 | CMAKE_ARGS -DCROW_BUILD_EXAMPLES=OFF -DCROW_BUILD_TESTS=OFF -DCROW_ENABLE_SSL=OFF 38 | ) 39 | FetchContent_MakeAvailable(Crow) 40 | 41 | # Fetch cpp-httplib dependency 42 | FetchContent_Declare( 43 | httplib 44 | GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git 45 | GIT_TAG v0.15.3 # Use a recent stable tag 46 | ) 47 | FetchContent_MakeAvailable(httplib) 48 | 49 | # Find OpenSSL for HTTPS support in cpp-httplib 50 | find_package(OpenSSL REQUIRED) 51 | 52 | # --- Executable --- 53 | add_executable(google_auth_example main.cpp) 54 | 55 | target_link_libraries(google_auth_example PRIVATE 56 | minioauth2 57 | Crow::Crow 58 | httplib::httplib 59 | OpenSSL::SSL 60 | OpenSSL::Crypto 61 | # Asio might be linked via Crow, or link explicitly if needed: 62 | # Asio::asio # This target might be available if Asio creates one. 63 | # For header-only Asio, often just the include directory is enough, 64 | # which Crow should pick up via its own find_package(asio) call. 65 | ) 66 | 67 | # Add Asio include directory to the example executable if Crow doesn't do it. 68 | # This ensures that our main.cpp can find asio.hpp if it needs it directly, 69 | # or if Crow's headers include it in a way that requires it to be in the path. 70 | # Usually, Crow's target should handle this. 71 | if(TARGET asio) 72 | # If Asio provides a target (e.g. Asio::asio), it's better to link against it. 73 | # However, standalone Asio is often header-only. 74 | # We need to ensure its include path is available. 75 | # One way is to add to Crow's interface, or directly to our executable. 76 | # Crow should find it via find_package if asio_SOURCE_DIR/asio_BINARY_DIR is in CMAKE_PREFIX_PATH 77 | # or if asio creates an export set that find_package can use. 78 | # FetchContent_MakeAvailable should make asio findable. 79 | endif() 80 | 81 | # Optional: Define where to find nlohmann/json if needed directly here 82 | # and not linked via minioauth2 interface 83 | # if(TARGET nlohmann_json::nlohmann_json) 84 | # target_link_libraries(google_auth_example PRIVATE nlohmann_json::nlohmann_json) 85 | # endif() 86 | 87 | # Define the macro to enable OpenSSL support in cpp-httplib 88 | target_compile_definitions(google_auth_example 89 | PRIVATE 90 | CPPHTTPLIB_OPENSSL_SUPPORT 91 | ASIO_STANDALONE # Often needed by Crow 92 | ) 93 | 94 | # Ensure Crow headers are available 95 | target_include_directories(google_auth_example 96 | PRIVATE 97 | ${crow_SOURCE_DIR}/include 98 | ${httplib_SOURCE_DIR} 99 | ) 100 | 101 | install(TARGETS google_auth_example DESTINATION bin) -------------------------------------------------------------------------------- /examples/google_auth/README.md: -------------------------------------------------------------------------------- 1 | # MiniOAuth2 Google Authentication Example (Crow) 2 | 3 | This example demonstrates how to use the `MiniOAuth2` library with the [Crow C++ microframework](https://github.com/CrowCpp/Crow) to implement a "Login with Google" flow using the OAuth 2.0 Authorization Code Grant with PKCE. 4 | 5 | ## Features 6 | 7 | - Initiates the OAuth flow when visiting `/login`. 8 | - Redirects the user to Google for authentication and consent. 9 | - Handles the callback from Google at `/callback`. 10 | - Retrieves the authorization code and validates the `state` parameter. 11 | - Uses `MiniOAuth2` helpers to prepare the token exchange request. 12 | - **Note:** This example *does not* include an HTTP client to perform the actual token exchange. It only shows how to prepare the request. 13 | - Shows basic parsing of the token response (requires `nlohmann/json`). 14 | 15 | ## Prerequisites 16 | 17 | - A C++20 compliant compiler (GCC, Clang, MSVC). 18 | - CMake (version 3.16 or later). 19 | - Internet connection (for downloading dependencies). 20 | - A Google Cloud Project with OAuth 2.0 credentials. 21 | 22 | ## Setup 23 | 24 | 1. **Google Cloud Credentials:** 25 | * Go to the [Google Cloud Console](https://console.cloud.google.com/). 26 | * Create a new project or select an existing one. 27 | * Navigate to "APIs & Services" > "Credentials". 28 | * Click "Create Credentials" > "OAuth client ID". 29 | * Choose "Web application" as the application type. 30 | * Give it a name (e.g., "MiniOAuth2 Crow Example"). 31 | * Under "Authorized redirect URIs", add `http://localhost:18080/callback`. 32 | * Click "Create". 33 | * Copy the **Client ID** and **Client Secret**. You will need them shortly. 34 | 35 | 2. **Environment Variables:** 36 | Set the following environment variables in your terminal before running the example: 37 | * `GOOGLE_CLIENT_ID`: Your Google Client ID. 38 | * `GOOGLE_CLIENT_SECRET`: Your Google Client Secret. 39 | 40 | **Example (Bash/Zsh):** 41 | ```bash 42 | export GOOGLE_CLIENT_ID="YOUR_CLIENT_ID_HERE" 43 | export GOOGLE_CLIENT_SECRET="YOUR_CLIENT_SECRET_HERE" 44 | ``` 45 | 46 | **Example (PowerShell):** 47 | ```powershell 48 | $env:GOOGLE_CLIENT_ID="YOUR_CLIENT_ID_HERE" 49 | $env:GOOGLE_CLIENT_SECRET="YOUR_CLIENT_SECRET_HERE" 50 | ``` 51 | 52 | ## Build and Run 53 | 54 | 1. **Clone the main `MiniOAuth2` repository (if you haven't already):** 55 | ```bash 56 | git clone https://github.com/your-username/minioauth2.git # Replace with actual URL 57 | cd minioauth2 58 | ``` 59 | 60 | 2. **Configure CMake (from the root `minioauth2` directory):** 61 | This command enables building examples and downloads dependencies (Crow, Asio, nlohmann/json). 62 | ```bash 63 | cmake -B build -S . -DMINIOAUTH2_BUILD_EXAMPLES=ON -DMINIOAUTH2_USE_NLOHMANN_JSON=ON 64 | ``` 65 | 66 | 3. **Build the project:** 67 | ```bash 68 | cmake --build build 69 | ``` 70 | (On Windows with Visual Studio, you might need to specify the configuration, e.g., `cmake --build build --config Debug` or `Release`) 71 | 72 | 4. **Run the example (ensure environment variables are set):** 73 | The executable will be inside the `build` directory. 74 | ```bash 75 | ./build/examples/google_auth/google_auth_example 76 | ``` 77 | (On Windows, the path might be `./build/examples/google_auth/Debug/google_auth_example.exe` or similar) 78 | 79 | 5. **Open your browser** and navigate to `http://localhost:18080`. 80 | 81 | 6. Click the "Login with Google" button and follow the Google authentication flow. 82 | 83 | 7. After successful authentication and consent, you will be redirected back to the `/callback` endpoint. Since this example lacks an HTTP client, you will see a message indicating that the token exchange request was prepared but not sent. 84 | 85 | ## Notes 86 | 87 | - **Security:** The state/code_verifier storage in this example uses a simple `std::map` with a mutex. **This is not secure or scalable for production.** Use proper server-side session management (e.g., secure cookies, Redis, database) in a real application. 88 | - **HTTP Client:** To complete the flow, you would need to add an HTTP client library (like [cpr](https://github.com/libcpr/cpr), [cpp-httplib](https://github.com/yhirose/cpp-httplib)) to send the `TokenExchangeRequest` prepared by `MiniOAuth2` to Google's token endpoint. -------------------------------------------------------------------------------- /examples/google_auth/main.cpp: -------------------------------------------------------------------------------- 1 | #include "crow.h" 2 | #include "minioauth2.hpp" 3 | #define CPPHTTPLIB_OPENSSL_SUPPORT // Ensure this is defined before including httplib.h 4 | #include "httplib.h" // Include cpp-httplib 5 | #include 6 | #include 7 | #include // std::getenv 8 | #include 9 | #include 10 | #include 11 | #include // Include for std::runtime_error 12 | #include // Include for std::move 13 | #include // Include for parsing host/path 14 | 15 | // --- Configuration --- 16 | // Load from environment variables for security 17 | const char* google_client_id_env = std::getenv("GOOGLE_CLIENT_ID"); 18 | const char* google_client_secret_env = std::getenv("GOOGLE_CLIENT_SECRET"); 19 | const std::string redirect_uri = "http://localhost:18080/callback"; // Must match Google Console config 20 | 21 | // --- Temporary State/Verifier Storage --- 22 | // WARNING: THIS IS HIGHLY INSECURE AND FOR DEMONSTRATION PURPOSES ONLY! 23 | // In a real production environment, you MUST use a secure, server-side session mechanism. 24 | // Options include: 25 | // - Secure HTTP-only cookies mapping to server-side storage (e.g., Redis, database, secure session middleware). 26 | // - Encrypted JWTs (if storing state directly in the client, ensure strong encryption and short expiry). 27 | // This simple map has critical flaws for production: 28 | // - Data loss on server restart. 29 | // - Memory leaks (no entry expiration or cleanup). 30 | // - Not scalable across multiple server instances. 31 | // - Basic mutex only prevents data races, not other session vulnerabilities (e.g., fixation). 32 | std::map state_to_verifier_map; 33 | std::mutex map_mutex; // Basic protection for the map 34 | 35 | int main() 36 | { 37 | if (!google_client_id_env || !google_client_secret_env) { 38 | std::cerr << "Error: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables must be set." << std::endl; 39 | return 1; 40 | } 41 | 42 | minioauth2::OAuthConfig google_config = minioauth2::config::Google(); 43 | google_config.client_id = google_client_id_env; 44 | google_config.client_secret = google_client_secret_env; 45 | google_config.redirect_uri = redirect_uri; 46 | 47 | crow::SimpleApp app; 48 | 49 | CROW_ROUTE(app, "/login") 50 | ([&](const crow::request& /*req*/, crow::response& res){ 51 | try { 52 | // 1. Build the authorization request 53 | auto auth_request = minioauth2::build_authorization_request(google_config); 54 | 55 | // 2. Store state -> code_verifier mapping (INSECURE - use proper sessions! See WARNING above) 56 | { 57 | std::lock_guard lock(map_mutex); 58 | state_to_verifier_map[auth_request.state] = auth_request.code_verifier; 59 | // TODO: Implement proper session cleanup/expiration in a real application 60 | } 61 | 62 | std::cout << "Redirecting user to: " << auth_request.authorization_url << std::endl; 63 | 64 | // 3. Redirect the user 65 | res.redirect(auth_request.authorization_url); 66 | res.end(); 67 | 68 | } catch (const std::exception& e) { 69 | std::cerr << "Error during /login: " << e.what() << std::endl; 70 | res.code = 500; 71 | res.write("Internal Server Error during login."); 72 | res.end(); 73 | } 74 | }); 75 | 76 | CROW_ROUTE(app, "/callback") 77 | ([&](const crow::request& req){ 78 | std::string token_response_body; 79 | try { 80 | // 1. Parse query parameters 81 | // Get the query string part from the request URL 82 | std::string query_string; 83 | size_t q_pos = req.raw_url.find('?'); 84 | if (q_pos != std::string::npos) { 85 | query_string = req.raw_url.substr(q_pos + 1); 86 | } 87 | auto params = minioauth2::parse_query_params(query_string); 88 | 89 | auto code_it = params.find("code"); 90 | auto state_it = params.find("state"); 91 | auto error_it = params.find("error"); 92 | 93 | if (error_it != params.end()) { 94 | std::cerr << "OAuth Error: " << params["error"] << std::endl; 95 | return crow::response(400, "OAuth provider returned an error: " + params["error"]); 96 | } 97 | 98 | if (code_it == params.end() || state_it == params.end()) { 99 | return crow::response(400, "Missing 'code' or 'state' in callback parameters."); 100 | } 101 | 102 | std::string code = code_it->second; 103 | std::string received_state = state_it->second; 104 | 105 | // 2. Validate state and retrieve code_verifier (INSECURE - use proper sessions! See WARNING above) 106 | std::string code_verifier; 107 | { 108 | std::lock_guard lock(map_mutex); 109 | auto verifier_it = state_to_verifier_map.find(received_state); 110 | if (verifier_it == state_to_verifier_map.end()) { 111 | std::cerr << "Error: Invalid or expired state parameter received." << std::endl; 112 | return crow::response(400, "Invalid state parameter."); 113 | } 114 | code_verifier = verifier_it->second; 115 | state_to_verifier_map.erase(verifier_it); // Consume state 116 | } 117 | 118 | std::cout << "Received callback. Code: " << code << ", State: " << received_state << std::endl; 119 | 120 | // 3. Prepare the token exchange request (using minioauth2) 121 | auto token_req = minioauth2::build_token_exchange_request(google_config, code, code_verifier); 122 | 123 | std::cout << "\n--- Preparing Token Exchange Request --- " << std::endl; 124 | std::cout << "URL: " << token_req.url << std::endl; 125 | std::cout << "Method: " << token_req.method << std::endl; 126 | std::cout << "Headers: "; 127 | for(const auto& pair : token_req.headers) { 128 | std::cout << pair.first << ": " << pair.second << "; "; 129 | } 130 | std::cout << std::endl; 131 | std::cout << "Body: " << token_req.body << std::endl; 132 | std::cout << "----------------------------------------\n" << std::endl; 133 | 134 | // 4. *** Perform the Token Exchange POST request using cpp-httplib *** 135 | std::cout << "Attempting token exchange..." << std::endl; 136 | 137 | // Parse URL to get host and path for httplib::Client 138 | std::smatch match; 139 | std::regex url_regex(R"(^(https?):\/\/([^\/]+)(\/.*)?$)"); 140 | std::string url_str = token_req.url; 141 | std::string host, path; 142 | if (std::regex_match(url_str, match, url_regex) && match.size() >= 3) { 143 | host = match[2].str(); 144 | path = match.size() >= 4 ? match[3].str() : "/"; 145 | if (path.empty()) path = "/"; 146 | } else { 147 | throw std::runtime_error("Could not parse token endpoint URL: " + url_str); 148 | } 149 | 150 | std::cout << "Parsed URL - Host: " << host << ", Path: " << path << std::endl; 151 | 152 | // Create HTTPS client (requires OpenSSL) 153 | httplib::Client cli(std::string("https://") + host); // Prepend https:// 154 | cli.enable_server_certificate_verification(true); // Recommended for production 155 | // You might need to configure CA cert path/bundle depending on your system: 156 | // const char * ca_cert_path = std::getenv("SSL_CERT_FILE"); 157 | // if (ca_cert_path) { 158 | // cli.set_ca_cert_path(ca_cert_path); 159 | // } else { 160 | // // Attempt default locations or log a warning 161 | // // Example: cli.set_ca_cert_path("./ca-bundle.pem"); 162 | // std::cout << "Warning: SSL_CERT_FILE env var not set. Using system default CA certs, verification might fail." << std::endl; 163 | // } 164 | 165 | // Convert map headers to httplib::Headers 166 | httplib::Headers http_headers; 167 | for(const auto& pair : token_req.headers) { 168 | http_headers.emplace(pair.first, pair.second); 169 | } 170 | 171 | // Send POST request 172 | auto http_res = cli.Post(path.c_str(), http_headers, token_req.body, "application/x-www-form-urlencoded"); 173 | 174 | if (!http_res) { 175 | // Handle transport errors (connection failed, timeout, etc.) 176 | auto err = http_res.error(); 177 | std::string error_msg = "HTTP request failed: " + httplib::to_string(err); 178 | std::cerr << error_msg << std::endl; 179 | // Depending on the error, you might check if cli.is_ssl() and provide OpenSSL error details 180 | // unsigned long ssl_err = cli.get_openssl_verify_result(); 181 | // if (ssl_err != X509_V_OK) { ... } 182 | throw std::runtime_error(error_msg); 183 | } 184 | 185 | std::cout << "Token exchange response status: " << http_res->status << std::endl; 186 | std::cout << "Token exchange response body: " << http_res->body << std::endl; 187 | 188 | if (http_res->status != 200) { 189 | std::cerr << "Token exchange failed with status: " << http_res->status << std::endl; 190 | throw std::runtime_error("Token exchange failed. Status: " + std::to_string(http_res->status) + ", Body: " + http_res->body); 191 | } 192 | 193 | token_response_body = http_res->body; 194 | 195 | // 5. Parse the token response (Now using the actual response) 196 | #ifdef MINIOAUTH2_USE_NLOHMANN_JSON 197 | try { 198 | // Explicitly cast to string_view to resolve ambiguity 199 | auto token_response = minioauth2::parse_token_response(std::string_view{token_response_body}); 200 | std::cout << "Access Token: " << token_response.access_token << std::endl; 201 | if(token_response.id_token) { 202 | std::cout << "ID Token received." << std::endl; 203 | // TODO: Validate ID token! 204 | // Try parsing payload (NO VALIDATION) 205 | auto payload = minioauth2::parse_jwt_payload(*token_response.id_token); 206 | if (payload && payload->contains("email")) { 207 | std::string user_email = payload->value("email", "(email not found in ID token)"); 208 | return crow::response(200, "Login Successful! Welcome, " + user_email + "
(Token parsing successful)"); 209 | } 210 | } 211 | 212 | return crow::response(200, "Login Successful! Welcome, (Not implemented yet - requires userinfo request)"); 213 | 214 | } catch (const nlohmann::json::exception& e) { // More specific catch 215 | std::cerr << "Failed to parse token JSON response: " << e.what() << std::endl; 216 | return crow::response(500, "Failed to parse token response JSON: " + std::string(e.what())); 217 | } catch (const std::runtime_error& e) { // Catch other minioauth2 errors 218 | std::cerr << "Error processing token response: " << e.what() << std::endl; 219 | return crow::response(500, "Error processing token response: " + std::string(e.what())); 220 | } 221 | #else 222 | return crow::response(501, "Token parsing requires nlohmann/json. Enable MINIOAUTH2_USE_NLOHMANN_JSON."); 223 | #endif 224 | 225 | } catch (const std::exception& e) { 226 | std::cerr << "Error during /callback: " << e.what() << std::endl; 227 | // Avoid leaking sensitive info like full body in production errors 228 | std::string error_details = e.what(); 229 | return crow::response(500, "Internal Server Error during callback: " + error_details); 230 | } 231 | }); 232 | 233 | CROW_ROUTE(app, "/") 234 | ([](){ 235 | return R"( 236 |

MiniOAuth2 Google Example (Crow)

237 |

Click the button to log in with your Google account.

238 |
239 | 240 |
241 |

Note: Ensure GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables are set.

242 | )"; 243 | }); 244 | 245 | // Basic error handling 246 | // app.handle_upgrade = [&](const crow::request& /*req*/, const crow::response& /*res*/, asio::ip::tcp::socket&& /*sock*/) {}; // Comment out 247 | // app.handle_after_request = [&](crow::request& req, crow::response& res, crow::routing_params& params) {}; // Comment out for now 248 | // app.handle_http_error = [&](const crow::request& req, crow::response& res, int status_code) { // Comment out for now 249 | 250 | std::cout << "Server starting on http://localhost:18080" << std::endl; 251 | std::cout << "Visit http://localhost:18080 to begin." << std::endl; 252 | std::cout << "Login endpoint: /login" << std::endl; 253 | std::cout << "Callback endpoint: /callback" << std::endl; 254 | 255 | app.port(18080).multithreaded().run(); 256 | 257 | return 0; 258 | } -------------------------------------------------------------------------------- /include/minioauth2.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include // For std::getenv 16 | #include // For std::isxdigit, std::tolower 17 | #include // Needed by PicoSHA2 and potentially others 18 | 19 | #ifdef MINIOAUTH2_USE_NLOHMANN_JSON 20 | #include 21 | #endif 22 | 23 | /** 24 | * @brief Main namespace for the MiniOAuth2 library. 25 | */ 26 | namespace minioauth2 { 27 | 28 | //---------------------------------------------------------------------- 29 | // Configuration 30 | //---------------------------------------------------------------------- 31 | 32 | /** 33 | * @struct OAuthConfig 34 | * @brief Holds the necessary configuration for an OAuth 2.0 provider. 35 | */ 36 | struct OAuthConfig { 37 | std::string client_id; ///< The client ID issued by the authorization server. 38 | std::string client_secret; ///< The client secret (needed for token exchange with some providers). 39 | std::string authorization_endpoint; ///< The authorization endpoint URL. 40 | std::string token_endpoint; ///< The token endpoint URL. 41 | std::string redirect_uri; ///< The client's redirect URI. 42 | std::vector default_scopes; ///< Default scopes to request if not overridden. 43 | }; 44 | 45 | /** 46 | * @brief Predefined configurations for common OAuth providers. 47 | */ 48 | namespace config { 49 | /** 50 | * @brief Provides a default configuration for Google OAuth 2.0. 51 | * @warning Client ID, Client Secret, and Redirect URI must be set by the user. 52 | * @return OAuthConfig for Google. 53 | */ 54 | inline OAuthConfig Google() { 55 | return { 56 | "", // Needs to be set by the application (e.g., from environment variables) 57 | "", // Needs to be set by the application 58 | "https://accounts.google.com/o/oauth2/v2/auth", 59 | "https://oauth2.googleapis.com/token", 60 | "", // Needs to be set by the application 61 | {"openid", "profile", "email"} 62 | }; 63 | } 64 | } // namespace config 65 | 66 | //---------------------------------------------------------------------- 67 | // Internal Detail Namespace 68 | //---------------------------------------------------------------------- 69 | namespace detail { 70 | 71 | // ========================================================================== 72 | // PicoSHA2 Integration Start (MIT License) 73 | // Copyright (c) 2017 okdshin 74 | // https://github.com/okdshin/PicoSHA2 75 | // ========================================================================== 76 | /* 77 | The MIT License (MIT) 78 | 79 | Copyright (C) 2017 okdshin 80 | 81 | Permission is hereby granted, free of charge, to any person obtaining a copy 82 | of this software and associated documentation files (the "Software"), to deal 83 | in the Software without restriction, including without limitation the rights 84 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 85 | copies of the Software, and to permit persons to whom the Software is 86 | furnished to do so, subject to the following conditions: 87 | 88 | The above copyright notice and this permission notice shall be included in 89 | all copies or substantial portions of the Software. 90 | 91 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 92 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 93 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 94 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 95 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 96 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 97 | THE SOFTWARE. 98 | */ 99 | #ifndef PICOSHA2_H_INTERNAL // Avoid redefinition if user also includes it 100 | #define PICOSHA2_H_INTERNAL 101 | // picosha2:20140213 102 | 103 | #ifndef PICOSHA2_BUFFER_SIZE_FOR_INPUT_ITERATOR 104 | #define PICOSHA2_BUFFER_SIZE_FOR_INPUT_ITERATOR 1048576 105 | #endif 106 | 107 | #include // Already included but good practice 108 | #include 109 | #include // Already included 110 | #include // Already included 111 | #include // Already included 112 | 113 | // Wrap picosha2 code in its own nested namespace within detail 114 | namespace picosha2 { 115 | typedef unsigned long word_t; 116 | typedef unsigned char byte_t; 117 | 118 | static const size_t k_digest_size = 32; 119 | 120 | namespace detail { 121 | inline byte_t mask_8bit(byte_t x) { return x & 0xff; } 122 | 123 | inline word_t mask_32bit(word_t x) { return x & 0xffffffff; } 124 | 125 | const word_t add_constant[64] = { 126 | 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 127 | 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 128 | 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 129 | 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 130 | 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 131 | 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 132 | 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 133 | 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 134 | 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 135 | 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 136 | 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2}; 137 | 138 | const word_t initial_message_digest[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 139 | 0xa54ff53a, 0x510e527f, 0x9b05688c, 140 | 0x1f83d9ab, 0x5be0cd19}; 141 | 142 | inline word_t ch(word_t x, word_t y, word_t z) { return (x & y) ^ ((~x) & z); } 143 | 144 | inline word_t maj(word_t x, word_t y, word_t z) { 145 | return (x & y) ^ (x & z) ^ (y & z); 146 | } 147 | 148 | inline word_t rotr(word_t x, std::size_t n) { 149 | assert(n < 32); 150 | return mask_32bit((x >> n) | (x << (32 - n))); 151 | } 152 | 153 | inline word_t bsig0(word_t x) { return rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22); } 154 | 155 | inline word_t bsig1(word_t x) { return rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25); } 156 | 157 | inline word_t shr(word_t x, std::size_t n) { 158 | assert(n < 32); 159 | return x >> n; 160 | } 161 | 162 | inline word_t ssig0(word_t x) { return rotr(x, 7) ^ rotr(x, 18) ^ shr(x, 3); } 163 | 164 | inline word_t ssig1(word_t x) { return rotr(x, 17) ^ rotr(x, 19) ^ shr(x, 10); } 165 | 166 | template 167 | void hash256_block(RaIter1 message_digest, RaIter2 first, RaIter2 last) { 168 | assert(first + 64 == last); 169 | static_cast(last); // for avoiding unused-variable warning 170 | word_t w[64]; 171 | std::fill(w, w + 64, word_t(0)); 172 | for (std::size_t i = 0; i < 16; ++i) { 173 | w[i] = (static_cast(mask_8bit(*(first + i * 4))) << 24) | 174 | (static_cast(mask_8bit(*(first + i * 4 + 1))) << 16) | 175 | (static_cast(mask_8bit(*(first + i * 4 + 2))) << 8) | 176 | (static_cast(mask_8bit(*(first + i * 4 + 3)))); 177 | } 178 | for (std::size_t i = 16; i < 64; ++i) { 179 | w[i] = mask_32bit(ssig1(w[i - 2]) + w[i - 7] + ssig0(w[i - 15]) + 180 | w[i - 16]); 181 | } 182 | 183 | word_t a = *message_digest; 184 | word_t b = *(message_digest + 1); 185 | word_t c = *(message_digest + 2); 186 | word_t d = *(message_digest + 3); 187 | word_t e = *(message_digest + 4); 188 | word_t f = *(message_digest + 5); 189 | word_t g = *(message_digest + 6); 190 | word_t h = *(message_digest + 7); 191 | 192 | for (std::size_t i = 0; i < 64; ++i) { 193 | word_t temp1 = h + bsig1(e) + ch(e, f, g) + add_constant[i] + w[i]; 194 | word_t temp2 = bsig0(a) + maj(a, b, c); 195 | h = g; 196 | g = f; 197 | f = e; 198 | e = mask_32bit(d + temp1); 199 | d = c; 200 | c = b; 201 | b = a; 202 | a = mask_32bit(temp1 + temp2); 203 | } 204 | *message_digest += a; 205 | *(message_digest + 1) += b; 206 | *(message_digest + 2) += c; 207 | *(message_digest + 3) += d; 208 | *(message_digest + 4) += e; 209 | *(message_digest + 5) += f; 210 | *(message_digest + 6) += g; 211 | *(message_digest + 7) += h; 212 | for (std::size_t i = 0; i < 8; ++i) { 213 | *(message_digest + i) = mask_32bit(*(message_digest + i)); 214 | } 215 | } 216 | 217 | } // namespace detail 218 | 219 | template 220 | void output_hex(InIter first, InIter last, std::ostream& os) { 221 | os.setf(std::ios::hex, std::ios::basefield); 222 | while (first != last) { 223 | os.width(2); 224 | os.fill('0'); 225 | os << static_cast(*first); 226 | ++first; 227 | } 228 | os.setf(std::ios::dec, std::ios::basefield); 229 | } 230 | 231 | template 232 | void bytes_to_hex_string(InIter first, InIter last, std::string& hex_str) { 233 | std::ostringstream oss; 234 | output_hex(first, last, oss); 235 | hex_str.assign(oss.str()); 236 | } 237 | 238 | template 239 | void bytes_to_hex_string(const InContainer& bytes, std::string& hex_str) { 240 | bytes_to_hex_string(bytes.begin(), bytes.end(), hex_str); 241 | } 242 | 243 | template 244 | std::string bytes_to_hex_string(InIter first, InIter last) { 245 | std::string hex_str; 246 | bytes_to_hex_string(first, last, hex_str); 247 | return hex_str; 248 | } 249 | 250 | template 251 | std::string bytes_to_hex_string(const InContainer& bytes) { 252 | std::string hex_str; 253 | bytes_to_hex_string(bytes, hex_str); 254 | return hex_str; 255 | } 256 | 257 | class hash256_one_by_one { 258 | public: 259 | hash256_one_by_one() { init(); } 260 | 261 | void init() { 262 | buffer_.clear(); 263 | std::fill(data_length_digits_, data_length_digits_ + 4, word_t(0)); 264 | std::copy(picosha2::detail::initial_message_digest, // Need to qualify namespace here 265 | picosha2::detail::initial_message_digest + 8, h_); 266 | } 267 | 268 | template 269 | void process(RaIter first, RaIter last) { 270 | add_to_data_length(static_cast(std::distance(first, last))); 271 | std::copy(first, last, std::back_inserter(buffer_)); 272 | std::size_t i = 0; 273 | for (; i + 64 <= buffer_.size(); i += 64) { 274 | picosha2::detail::hash256_block(h_, buffer_.begin() + i, // Need to qualify namespace here 275 | buffer_.begin() + i + 64); 276 | } 277 | buffer_.erase(buffer_.begin(), buffer_.begin() + i); 278 | } 279 | 280 | void finish() { 281 | byte_t temp[64]; 282 | std::fill(temp, temp + 64, byte_t(0)); 283 | std::size_t remains = buffer_.size(); 284 | std::copy(buffer_.begin(), buffer_.end(), temp); 285 | assert(remains < 64); 286 | 287 | // This branch is not executed actually (`remains` is always lower than 64), 288 | // but needed to avoid g++ false-positive warning. 289 | // See https://github.com/okdshin/PicoSHA2/issues/25 290 | // vvvvvvvvvvvvvvvv 291 | if(remains >= 64) { 292 | remains = 63; 293 | } 294 | // ^^^^^^^^^^^^^^^^ 295 | 296 | temp[remains] = 0x80; 297 | 298 | if (remains > 55) { 299 | std::fill(temp + remains + 1, temp + 64, byte_t(0)); 300 | picosha2::detail::hash256_block(h_, temp, temp + 64); // Need to qualify namespace here 301 | std::fill(temp, temp + 64 - 4, byte_t(0)); // Should be 56? Check original code. It's temp + 64 - 8. Corrected: 302 | //std::fill(temp, temp + 56, byte_t(0)); 303 | } else { 304 | //std::fill(temp + remains + 1, temp + 64 - 4, byte_t(0)); // Corrected: 305 | std::fill(temp + remains + 1, temp + 56, byte_t(0)); 306 | } 307 | 308 | write_data_bit_length(&(temp[56])); 309 | picosha2::detail::hash256_block(h_, temp, temp + 64); // Need to qualify namespace here 310 | } 311 | 312 | template 313 | void get_hash_bytes(OutIter first, OutIter last) const { 314 | for (const word_t* iter = h_; iter != h_ + 8; ++iter) { 315 | for (std::size_t i = 0; i < 4 && first != last; ++i) { 316 | *(first++) = picosha2::detail::mask_8bit( // Need to qualify namespace here 317 | static_cast((*iter >> (24 - 8 * i)))); 318 | } 319 | } 320 | } 321 | 322 | private: 323 | void add_to_data_length(word_t n) { 324 | word_t carry = 0; 325 | data_length_digits_[0] += n; 326 | for (std::size_t i = 0; i < 4; ++i) { 327 | data_length_digits_[i] += carry; 328 | if (data_length_digits_[i] >= 65536u) { 329 | carry = data_length_digits_[i] >> 16; 330 | data_length_digits_[i] &= 65535u; 331 | } else { 332 | break; 333 | } 334 | } 335 | } 336 | void write_data_bit_length(byte_t* begin) { 337 | word_t data_bit_length_digits[4]; 338 | std::copy(data_length_digits_, data_length_digits_ + 4, 339 | data_bit_length_digits); 340 | 341 | // convert byte length to bit length (multiply 8 or shift 3 times left) 342 | word_t carry = 0; 343 | for (std::size_t i = 0; i < 4; ++i) { 344 | word_t before_val = data_bit_length_digits[i]; 345 | data_bit_length_digits[i] <<= 3; 346 | data_bit_length_digits[i] |= carry; 347 | data_bit_length_digits[i] &= 65535u; 348 | carry = (before_val >> (16 - 3)) & 65535u; 349 | } 350 | 351 | // write data_bit_length (Big Endian order) 352 | for (int i = 3; i >= 0; --i) { 353 | (*begin++) = static_cast(data_bit_length_digits[i] >> 8); 354 | (*begin++) = static_cast(data_bit_length_digits[i]); 355 | } 356 | } 357 | std::vector buffer_; 358 | word_t data_length_digits_[4]; // as 64bit integer (16bit x 4 integer stored in little endian order of 16-bit chunks) 359 | word_t h_[8]; 360 | }; 361 | 362 | inline void get_hash_hex_string(const hash256_one_by_one& hasher, 363 | std::string& hex_str) { 364 | byte_t hash[k_digest_size]; 365 | hasher.get_hash_bytes(hash, hash + k_digest_size); 366 | return bytes_to_hex_string(hash, hash + k_digest_size, hex_str); 367 | } 368 | 369 | inline std::string get_hash_hex_string(const hash256_one_by_one& hasher) { 370 | std::string hex_str; 371 | get_hash_hex_string(hasher, hex_str); 372 | return hex_str; 373 | } 374 | 375 | namespace impl { 376 | template 377 | void hash256_impl(RaIter first, RaIter last, OutIter first2, OutIter last2, int, 378 | std::random_access_iterator_tag) { 379 | hash256_one_by_one hasher; 380 | // hasher.init(); // Constructor calls init() 381 | hasher.process(first, last); 382 | hasher.finish(); 383 | hasher.get_hash_bytes(first2, last2); 384 | } 385 | 386 | template 387 | void hash256_impl(InputIter first, InputIter last, OutIter first2, 388 | OutIter last2, int buffer_size, std::input_iterator_tag) { 389 | std::vector buffer(buffer_size); 390 | hash256_one_by_one hasher; 391 | // hasher.init(); // Constructor calls init() 392 | while (first != last) { 393 | int size = buffer_size; 394 | for (int i = 0; i != buffer_size; ++i, ++first) { 395 | if (first == last) { 396 | size = i; 397 | break; 398 | } 399 | buffer[i] = *first; 400 | } 401 | hasher.process(buffer.begin(), buffer.begin() + size); 402 | } 403 | hasher.finish(); 404 | hasher.get_hash_bytes(first2, last2); 405 | } 406 | } // namespace impl 407 | 408 | template 409 | void hash256(InIter first, InIter last, OutIter first2, OutIter last2, 410 | int buffer_size = PICOSHA2_BUFFER_SIZE_FOR_INPUT_ITERATOR) { 411 | picosha2::impl::hash256_impl( // Need to qualify namespace here 412 | first, last, first2, last2, buffer_size, 413 | typename std::iterator_traits::iterator_category()); 414 | } 415 | 416 | template 417 | void hash256(InIter first, InIter last, OutContainer& dst) { 418 | hash256(first, last, dst.begin(), dst.end()); 419 | } 420 | 421 | template 422 | void hash256(const InContainer& src, OutIter first, OutIter last) { 423 | hash256(src.begin(), src.end(), first, last); 424 | } 425 | 426 | template 427 | void hash256(const InContainer& src, OutContainer& dst) { 428 | hash256(src.begin(), src.end(), dst.begin(), dst.end()); 429 | } 430 | 431 | template 432 | void hash256_hex_string(InIter first, InIter last, std::string& hex_str) { 433 | byte_t hashed[k_digest_size]; 434 | hash256(first, last, hashed, hashed + k_digest_size); 435 | std::ostringstream oss; 436 | output_hex(hashed, hashed + k_digest_size, oss); 437 | hex_str.assign(oss.str()); 438 | } 439 | 440 | template 441 | std::string hash256_hex_string(InIter first, InIter last) { 442 | std::string hex_str; 443 | hash256_hex_string(first, last, hex_str); 444 | return hex_str; 445 | } 446 | 447 | inline void hash256_hex_string(const std::string& src, std::string& hex_str) { 448 | hash256_hex_string(src.begin(), src.end(), hex_str); 449 | } 450 | 451 | template 452 | void hash256_hex_string(const InContainer& src, std::string& hex_str) { 453 | hash256_hex_string(src.begin(), src.end(), hex_str); 454 | } 455 | 456 | template 457 | std::string hash256_hex_string(const InContainer& src) { 458 | return hash256_hex_string(src.begin(), src.end()); 459 | } 460 | } // namespace picosha2 461 | #endif // PICOSHA2_H_INTERNAL 462 | // ========================================================================== 463 | // PicoSHA2 Integration End 464 | // ========================================================================== 465 | 466 | 467 | /** 468 | * @brief Generates a string of random bytes. 469 | * @param len Number of bytes to generate. 470 | * @return String containing random bytes. 471 | */ 472 | inline std::string generate_random_bytes_internal(std::size_t len) { 473 | std::random_device rd; 474 | std::mt19937 gen(rd()); 475 | std::uniform_int_distribution<> distrib(0, 255); 476 | std::string result(len, '\\x20'); // Use hex escape for space 477 | std::generate_n(result.begin(), len, [&]() { return static_cast(distrib(gen)); }); 478 | return result; 479 | } 480 | 481 | /** 482 | * @brief Encodes a string to Base64 URL-safe format. 483 | * @param input The string to encode. 484 | * @return The Base64 URL-encoded string. 485 | */ 486 | inline std::string base64_url_encode(std::string_view input) { 487 | // Uses picosha2::bytes_to_hex_string implicitly if needed? No. Separate. 488 | // This function remains the same. 489 | const std::string_view base64_chars = 490 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 491 | "abcdefghijklmnopqrstuvwxyz" 492 | "0123456789-_"; // URL-safe variant 493 | 494 | std::string encoded; 495 | encoded.reserve(((input.length() / 3) + (input.length() % 3 > 0)) * 4); 496 | 497 | int val = 0; 498 | int bits = -6; 499 | for (unsigned char c : input) { 500 | val = (val << 8) + c; 501 | bits += 8; 502 | while (bits >= 0) { 503 | encoded.push_back(base64_chars[(val >> bits) & 0x3F]); 504 | bits -= 6; 505 | } 506 | } 507 | 508 | if (bits > -6) { 509 | encoded.push_back(base64_chars[((val << 8) >> (bits + 8)) & 0x3F]); 510 | } 511 | // No padding for base64url 512 | return encoded; 513 | } 514 | 515 | /** 516 | * @brief Decodes a Base64 URL-safe encoded string. 517 | * @param input The Base64 URL-encoded string. 518 | * @return The decoded string, or std::nullopt if decoding fails. 519 | * @note Handles input without padding. 520 | */ 521 | inline std::optional base64_url_decode(std::string_view input) { 522 | // This function remains the same. 523 | const std::string base64_chars_with_padding = 524 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 525 | "abcdefghijklmnopqrstuvwxyz" 526 | "0123456789-_="; // Includes padding char for lookup 527 | 528 | std::string_view temp_input = input; 529 | 530 | std::string decoded; 531 | decoded.reserve(input.length() * 3 / 4); // Approximate decoded length 532 | 533 | std::vector T(256, -1); 534 | for (int i = 0; i < 64; i++) T["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"[i]] = i; 535 | 536 | int val = 0; 537 | int bits = -8; // Start with -8 to ensure the first character is processed correctly 538 | for (char c : temp_input) { 539 | if (T[static_cast(c)] == -1) { // Invalid character 540 | if (c == '=') break; // Padding character, stop processing 541 | return std::nullopt; // Or throw an error 542 | } 543 | val = (val << 6) + T[static_cast(c)]; 544 | bits += 6; 545 | if (bits >= 0) { 546 | decoded.push_back(static_cast((val >> bits) & 0xFF)); 547 | bits -= 8; 548 | } 549 | } 550 | return decoded; 551 | } 552 | 553 | 554 | /** 555 | * @brief Computes SHA256 hash of a string using PicoSHA2. 556 | * @param input The string to hash. 557 | * @return A string containing the raw 32-byte SHA256 hash. 558 | */ 559 | inline std::string sha256(std::string_view input) { 560 | std::vector hash_vec(picosha2::k_digest_size); 561 | // Use iterator-based function from PicoSHA2 (now defined above) 562 | picosha2::hash256(input.begin(), input.end(), hash_vec.begin(), hash_vec.end()); 563 | 564 | // Convert vector of bytes to std::string 565 | return std::string(reinterpret_cast(hash_vec.data()), hash_vec.size()); 566 | } 567 | 568 | /** 569 | * @brief URL-encodes a string. 570 | * @param value The string to encode. 571 | * @return The URL-encoded string. 572 | */ 573 | inline std::string url_encode(std::string_view value) { 574 | // This function remains the same. 575 | std::ostringstream encoded; 576 | encoded << std::fixed << std::setprecision(0); 577 | 578 | for (char c : value) { 579 | if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '~') { 580 | encoded << c; 581 | } else { 582 | encoded << '%' << std::uppercase << std::setw(2) << std::setfill('0') << std::hex << static_cast(static_cast(c)); 583 | } 584 | } 585 | return encoded.str(); 586 | } 587 | 588 | /** 589 | * @brief URL-decodes a string. 590 | * @param value The string to decode. 591 | * @return The URL-decoded string. 592 | * @throws std::runtime_error if decoding encounters invalid hex characters. 593 | */ 594 | inline std::string url_decode(std::string_view value) { 595 | // This function remains the same. 596 | std::ostringstream decoded; 597 | for (size_t i = 0; i < value.length(); ++i) { 598 | if (value[i] == '%') { 599 | if (i + 2 < value.length() && std::isxdigit(static_cast(value[i+1])) && std::isxdigit(static_cast(value[i+2]))) { 600 | std::string hex_byte_str = std::string(value.substr(i + 1, 2)); 601 | try { 602 | char byte = static_cast(std::stoi(hex_byte_str, nullptr, 16)); 603 | decoded << byte; 604 | } catch (const std::invalid_argument& e) { 605 | throw std::runtime_error("URL Decode: Invalid hex characters in % escape: " + hex_byte_str); 606 | } catch (const std::out_of_range& e) { 607 | throw std::runtime_error("URL Decode: Hex value out of range in % escape: " + hex_byte_str); 608 | } 609 | i += 2; 610 | } else { 611 | decoded << '%'; // Or handle error 612 | } 613 | } else if (value[i] == '+') { 614 | decoded << ' '; 615 | } else { 616 | decoded << value[i]; 617 | } 618 | } 619 | return decoded.str(); 620 | } 621 | 622 | } // namespace detail 623 | 624 | //---------------------------------------------------------------------- 625 | // Public API 626 | //---------------------------------------------------------------------- 627 | 628 | /** 629 | * @brief Generates a cryptographically secure random string suitable for 'state' or 'code_verifier'. 630 | * @param length The desired length of the string. RFC 7636 recommends 43-128 chars for code_verifier. 631 | * Minimum 32 characters is generally advised for good entropy. 632 | * @return A random alphanumeric string (A-Z, a-z, 0-9). 633 | * @note Uses std::random_device for seeding, which aims for non-deterministic random numbers. 634 | */ 635 | inline std::string generate_random_string(std::size_t length = 43) { 636 | if (length == 0) return ""; 637 | // Using a more restricted charset for general compatibility, 638 | // but PKCE verifiers can use unreserved characters: A-Z / a-z / 0-9 / "-" / "." / "_" / "~" 639 | const std::string_view chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 640 | std::string result(length, '\x20'); // Use hex escape for space 641 | std::random_device rd; 642 | std::mt19937 gen(rd()); 643 | std::uniform_int_distribution<> distrib(0, chars.length() - 1); 644 | std::generate_n(result.begin(), length, [&]() { return chars[distrib(gen)]; }); 645 | return result; 646 | } 647 | 648 | /** 649 | * @brief Generates the PKCE code challenge from a code verifier. 650 | * @param code_verifier The code verifier string. 651 | * @param method The challenge method. Supported: "plain", "S256". 652 | * @return The generated code challenge string. 653 | * @throws std::runtime_error If an unsupported method is requested or if S256 is used without a functional SHA256 implementation. 654 | * @warning If using "S256", ensure `detail::sha256` has a real cryptographic hash implementation. 655 | * The default placeholder is insecure. 656 | */ 657 | inline std::string generate_code_challenge(std::string_view code_verifier, std::string_view method = "S256") { 658 | if (method == "plain") { 659 | return std::string(code_verifier); 660 | } else if (method == "S256") { 661 | // IMPORTANT: The detail::sha256 function MUST be a real SHA256 implementation. 662 | // The current placeholder is insecure. 663 | std::string hashed_verifier = detail::sha256(code_verifier); 664 | if (hashed_verifier.length() != 32) { // SHA256 produces 32 bytes 665 | throw std::runtime_error("S256 code challenge generation failed: SHA256 output is not 32 bytes. Ensure SHA256 is correctly implemented."); 666 | } 667 | return detail::base64_url_encode(hashed_verifier); 668 | } else { 669 | throw std::runtime_error("Unsupported code challenge method: " + std::string(method)); 670 | } 671 | } 672 | 673 | /** 674 | * @struct AuthorizationRequest 675 | * @brief Holds all necessary components for initiating an authorization request. 676 | */ 677 | struct AuthorizationRequest { 678 | std::string state; ///< Generated 'state' parameter. Store this to validate upon callback. 679 | std::string code_verifier; ///< Generated PKCE 'code_verifier'. Store this securely until token exchange. 680 | std::string code_challenge; ///< Generated PKCE 'code_challenge'. 681 | std::string code_challenge_method; ///< PKCE method used ("plain" or "S256"). 682 | std::string authorization_url; ///< The fully constructed authorization URL to redirect the user to. 683 | }; 684 | 685 | /** 686 | * @brief Prepares the parameters and URL to initiate the Authorization Code Flow with PKCE. 687 | * @param config The OAuth configuration for the provider. 688 | * @param scopes Optional list of specific scopes to request, overriding defaults in config. 689 | * @param pkce_method The PKCE method to use ("plain" or "S256"). Defaults to "S256". 690 | * @param custom_state Optional custom state value. If empty, a random one is generated. 691 | * @return An AuthorizationRequest struct containing generated values and the full URL. 692 | * @note It's highly recommended to use "S256" as the pkce_method for security. 693 | */ 694 | inline AuthorizationRequest build_authorization_request( 695 | const OAuthConfig& config, 696 | const std::vector& scopes = {}, 697 | std::string_view pkce_method = "S256", 698 | std::string_view custom_state = "") 699 | { 700 | AuthorizationRequest req; 701 | req.state = custom_state.empty() ? generate_random_string(32) : std::string(custom_state); 702 | req.code_verifier = generate_random_string(43); // Min 43 chars for PKCE 703 | req.code_challenge_method = std::string(pkce_method); 704 | req.code_challenge = generate_code_challenge(req.code_verifier, req.code_challenge_method); 705 | 706 | std::ostringstream url_ss; 707 | url_ss << config.authorization_endpoint; 708 | url_ss << "?response_type=code"; 709 | url_ss << "&client_id=" << detail::url_encode(config.client_id); 710 | url_ss << "&redirect_uri=" << detail::url_encode(config.redirect_uri); 711 | url_ss << "&state=" << detail::url_encode(req.state); 712 | url_ss << "&code_challenge=" << detail::url_encode(req.code_challenge); 713 | url_ss << "&code_challenge_method=" << req.code_challenge_method; 714 | 715 | const auto& scopes_to_use = scopes.empty() ? config.default_scopes : scopes; 716 | if (!scopes_to_use.empty()) { 717 | url_ss << "&scope="; 718 | for (size_t i = 0; i < scopes_to_use.size(); ++i) { 719 | url_ss << detail::url_encode(scopes_to_use[i]) << (i == scopes_to_use.size() - 1 ? "" : " "); 720 | } 721 | } 722 | // Add other optional parameters if needed, e.g., prompt, login_hint 723 | 724 | req.authorization_url = url_ss.str(); 725 | return req; 726 | } 727 | 728 | /** 729 | * @struct TokenExchangeRequest 730 | * @brief Holds parameters for the token exchange HTTP request. 731 | */ 732 | struct TokenExchangeRequest { 733 | std::string url; ///< The token endpoint URL. 734 | std::string method = "POST"; ///< HTTP method (always POST). 735 | std::map headers; ///< HTTP headers for the request. 736 | std::string body; ///< HTTP request body (form-urlencoded). 737 | }; 738 | 739 | /** 740 | * @brief Prepares the parameters for the token exchange POST request. 741 | * This function *does not* perform the HTTP request itself. The caller must use an HTTP client. 742 | * @param config The OAuth configuration. 743 | * @param authorization_code The authorization code received from the callback. 744 | * @param code_verifier The original PKCE code_verifier associated with this flow. 745 | * @return A TokenExchangeRequest struct with all necessary details for the HTTP POST. 746 | */ 747 | inline TokenExchangeRequest build_token_exchange_request( 748 | const OAuthConfig& config, 749 | std::string_view authorization_code, 750 | std::string_view code_verifier) 751 | { 752 | TokenExchangeRequest req; 753 | req.url = config.token_endpoint; 754 | req.headers["Content-Type"] = "application/x-www-form-urlencoded"; 755 | // Note: Basic Authentication for client_id:client_secret in Authorization header 756 | // is an alternative to sending them in the body for some providers. 757 | // Example: 758 | // if (!config.client_id.empty() && !config.client_secret.empty()) { 759 | // std::string creds = detail::base64_encode(config.client_id + ":" + config.client_secret); 760 | // req.headers["Authorization"] = "Basic " + creds; 761 | // } 762 | 763 | std::ostringstream body_ss; 764 | body_ss << "grant_type=authorization_code"; 765 | body_ss << "&code=" << detail::url_encode(authorization_code); 766 | body_ss << "&redirect_uri=" << detail::url_encode(config.redirect_uri); 767 | body_ss << "&client_id=" << detail::url_encode(config.client_id); 768 | if (!config.client_secret.empty()) { // Client secret might not be needed for public clients using PKCE 769 | body_ss << "&client_secret=" << detail::url_encode(config.client_secret); 770 | } 771 | body_ss << "&code_verifier=" << detail::url_encode(code_verifier); 772 | 773 | req.body = body_ss.str(); 774 | return req; 775 | } 776 | 777 | 778 | #ifdef MINIOAUTH2_USE_NLOHMANN_JSON 779 | /** 780 | * @struct TokenResponse 781 | * @brief Represents a parsed response from the token endpoint. 782 | * @note This structure is available only if `MINIOAUTH2_USE_NLOHMANN_JSON` is defined. 783 | */ 784 | struct TokenResponse { 785 | std::string access_token; ///< The access token. 786 | std::string token_type; ///< Type of token (e.g., "Bearer"). 787 | int expires_in = 0; ///< Token lifetime in seconds. 788 | std::optional refresh_token; ///< Optional refresh token. 789 | std::optional id_token; ///< Optional ID token (for OpenID Connect). 790 | std::optional scope; ///< Scopes granted with the token. 791 | nlohmann::json raw_response; ///< The full raw JSON response. 792 | }; 793 | 794 | /** 795 | * @brief Parses a JSON response body (as nlohmann::json object) from the token endpoint. 796 | * @param json_response The nlohmann::json object representing the response. 797 | * @return A TokenResponse struct. 798 | * @throws nlohmann::json::exception If parsing fails or required fields are missing. 799 | * @note This function is available only if `MINIOAUTH2_USE_NLOHMANN_JSON` is defined. 800 | */ 801 | inline TokenResponse parse_token_response(const nlohmann::json& json_response) { 802 | TokenResponse resp; 803 | resp.raw_response = json_response; 804 | 805 | if (!json_response.is_object()) { 806 | throw std::runtime_error("Token response is not a JSON object."); 807 | } 808 | 809 | resp.access_token = json_response.at("access_token").get(); 810 | resp.token_type = json_response.at("token_type").get(); 811 | resp.expires_in = json_response.value("expires_in", 0); // Gracefully handle if missing 812 | 813 | if (json_response.contains("refresh_token") && !json_response.at("refresh_token").is_null()) { 814 | resp.refresh_token = json_response.at("refresh_token").get(); 815 | } 816 | if (json_response.contains("id_token") && !json_response.at("id_token").is_null()) { 817 | resp.id_token = json_response.at("id_token").get(); 818 | } 819 | if (json_response.contains("scope") && !json_response.at("scope").is_null()) { 820 | resp.scope = json_response.at("scope").get(); 821 | } 822 | 823 | return resp; 824 | } 825 | 826 | /** 827 | * @brief Parses a JSON response body (as string) from the token endpoint. 828 | * @param response_body The string containing the JSON response. 829 | * @return A TokenResponse struct. 830 | * @throws std::runtime_error If JSON parsing fails or required fields are missing. 831 | * @note This function is available only if `MINIOAUTH2_USE_NLOHMANN_JSON` is defined. 832 | */ 833 | inline TokenResponse parse_token_response(std::string_view response_body) { 834 | try { 835 | nlohmann::json j = nlohmann::json::parse(response_body); 836 | return parse_token_response(j); 837 | } catch (const nlohmann::json::parse_error& e) { 838 | throw std::runtime_error("Failed to parse token response JSON: " + std::string(e.what()) + ". Response body: " + std::string(response_body)); 839 | } catch (const nlohmann::json::exception& e) { // Catches at() and get() errors 840 | throw std::runtime_error("Error accessing token response fields: " + std::string(e.what())); 841 | } 842 | } 843 | 844 | /** 845 | * @brief Parses the payload of a JWT string. 846 | * Does not validate the signature or any claims. Only decodes and parses the JSON payload. 847 | * @param jwt_string The JWT string (typically an id_token). 848 | * @return nlohmann::json object of the payload, or std::nullopt if parsing fails. 849 | * @note This function is available only if `MINIOAUTH2_USE_NLOHMANN_JSON` is defined. 850 | * @warning This function DOES NOT VALIDATE the JWT signature or claims like 'exp', 'aud', 'iss'. 851 | * For production use, a full JWT validation library is recommended if relying on claims. 852 | */ 853 | inline std::optional parse_jwt_payload(std::string_view jwt_string) { 854 | std::string::size_type pos1 = jwt_string.find('.'); 855 | if (pos1 == std::string::npos) return std::nullopt; // Not a JWT or malformed 856 | 857 | std::string::size_type pos2 = jwt_string.find('.', pos1 + 1); 858 | if (pos2 == std::string::npos) return std::nullopt; // Not a JWT or malformed 859 | 860 | std::string_view payload_b64url = jwt_string.substr(pos1 + 1, pos2 - (pos1 + 1)); 861 | 862 | std::optional decoded_payload_str = detail::base64_url_decode(payload_b64url); 863 | if (!decoded_payload_str) return std::nullopt; 864 | 865 | try { 866 | return nlohmann::json::parse(*decoded_payload_str); 867 | } catch (const nlohmann::json::parse_error&) { 868 | return std::nullopt; 869 | } 870 | } 871 | 872 | #endif // MINIOAUTH2_USE_NLOHMANN_JSON 873 | 874 | //---------------------------------------------------------------------- 875 | // Utility Functions 876 | //---------------------------------------------------------------------- 877 | 878 | /** 879 | * @brief Parses query parameters from a URL query string. 880 | * @param query_string The query string part of a URL (e.g., "code=abc&state=xyz"), without the leading '?'. 881 | * @return A map of key-value pairs. Values are URL-decoded. 882 | * @throws std::runtime_error if URL decoding of a parameter value fails. 883 | */ 884 | inline std::map parse_query_params(std::string_view query_string) { 885 | std::map params; 886 | std::string_view sv(query_string); 887 | 888 | // Remove leading '?' if present (though function doc says it shouldn't be) 889 | if (!sv.empty() && sv.front() == '?') { 890 | sv.remove_prefix(1); 891 | } 892 | 893 | while (!sv.empty()) { 894 | std::string_view::size_type ampersand_pos = sv.find('&'); 895 | std::string_view pair_sv = sv.substr(0, ampersand_pos); 896 | 897 | std::string_view::size_type equals_pos = pair_sv.find('='); 898 | if (equals_pos != std::string_view::npos) { 899 | std::string key(pair_sv.substr(0, equals_pos)); 900 | std::string value_encoded(pair_sv.substr(equals_pos + 1)); 901 | params[key] = detail::url_decode(value_encoded); 902 | } else if (!pair_sv.empty()) { 903 | params[std::string(pair_sv)] = ""; // Handle keys without values 904 | } 905 | 906 | if (ampersand_pos == std::string_view::npos) { 907 | break; 908 | } 909 | sv.remove_prefix(ampersand_pos + 1); 910 | } 911 | return params; 912 | } 913 | 914 | } // namespace minioauth2 -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | # Define the test executable 4 | add_executable(minioauth2_tests 5 | main.cpp 6 | test_utils.cpp 7 | # Add other test_*.cpp files here as they are created 8 | ) 9 | 10 | # Link against GoogleTest 11 | # gtest_main automatically provides a main() function 12 | target_link_libraries(minioauth2_tests 13 | PRIVATE 14 | GTest::gtest_main 15 | minioauth2 # Link to our library to make headers available 16 | ) 17 | 18 | # Include CTest support for running tests 19 | include(GoogleTest) 20 | gtest_discover_tests(minioauth2_tests) -------------------------------------------------------------------------------- /test/main.cpp: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | int main(int argc, char **argv) { 4 | ::testing::InitGoogleTest(&argc, argv); 5 | return RUN_ALL_TESTS(); 6 | } -------------------------------------------------------------------------------- /test/minioauth2_test.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN // Provides main() for Catch2 2 | #include 3 | #include "minioauth2.hpp" 4 | #include 5 | #include 6 | #include 7 | 8 | TEST_CASE("Random String Generation", "[random]") { 9 | std::string s1 = minioauth2::generate_random_string(32); 10 | std::string s2 = minioauth2::generate_random_string(32); 11 | 12 | REQUIRE(s1.length() == 32); 13 | REQUIRE(s2.length() == 32); 14 | REQUIRE(s1 != s2); // Extremely unlikely to be equal 15 | 16 | // Check charset (alphanumeric) 17 | std::string allowed_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 18 | for (char c : s1) { 19 | REQUIRE(allowed_chars.find(c) != std::string::npos); 20 | } 21 | 22 | REQUIRE(minioauth2::generate_random_string(0).empty()); 23 | REQUIRE(minioauth2::generate_random_string(10).length() == 10); 24 | } 25 | 26 | TEST_CASE("URL Encoding/Decoding", "[url]") { 27 | SECTION("Encoding") { 28 | REQUIRE(minioauth2::detail::url_encode("hello world") == "hello%20world"); 29 | REQUIRE(minioauth2::detail::url_encode("test@example.com") == "test%40example.com"); 30 | REQUIRE(minioauth2::detail::url_encode("special/:*?<>|") == "special%2F%3A%2A%3F%3C%3E%7C"); 31 | REQUIRE(minioauth2::detail::url_encode("-_.~") == "-_.~" ); // Unreserved characters 32 | REQUIRE(minioauth2::detail::url_encode("") == ""); 33 | } 34 | SECTION("Decoding") { 35 | REQUIRE(minioauth2::detail::url_decode("hello%20world") == "hello world"); 36 | REQUIRE(minioauth2::detail::url_decode("test%40example.com") == "test@example.com"); 37 | REQUIRE(minioauth2::detail::url_decode("special%2F%3A%2A%3F%3C%3E%7C") == "special/:*?<>|"); 38 | REQUIRE(minioauth2::detail::url_decode("-_.~") == "-_.~"); 39 | REQUIRE(minioauth2::detail::url_decode("") == ""); 40 | REQUIRE(minioauth2::detail::url_decode("hello+world") == "hello world"); // + to space 41 | REQUIRE(minioauth2::detail::url_decode("malformed%") == "malformed%"); // Handle malformed 42 | REQUIRE(minioauth2::detail::url_decode("malformed%2") == "malformed%2"); // Handle malformed 43 | REQUIRE_THROWS(minioauth2::detail::url_decode("invalid%FGhex")); // Invalid hex chars 44 | } 45 | } 46 | 47 | TEST_CASE("Base64 URL Encoding/Decoding", "[base64]") { 48 | SECTION("Encoding") { 49 | REQUIRE(minioauth2::detail::base64_url_encode("hello") == "aGVsbG8"); 50 | REQUIRE(minioauth2::detail::base64_url_encode("hello world 123") == "aGVsbG8gd29ybGQgMTIz"); 51 | // Test cases from RFC 4648 (Base64url) 52 | REQUIRE(minioauth2::detail::base64_url_encode("\x14\xfb\x9c\x03\xd9\x7e") == "FPucA9l-"); 53 | REQUIRE(minioauth2::detail::base64_url_encode("\x14\xfb\x9c\x03\xd9") == "FPucA9k"); 54 | REQUIRE(minioauth2::detail::base64_url_encode("\x14\xfb\x9c\x03") == "FPucAw"); 55 | REQUIRE(minioauth2::detail::base64_url_encode("") == ""); 56 | } 57 | SECTION("Decoding") { 58 | REQUIRE(minioauth2::detail::base64_url_decode("aGVsbG8").value() == "hello"); 59 | REQUIRE(minioauth2::detail::base64_url_decode("aGVsbG8gd29ybGQgMTIz").value() == "hello world 123"); 60 | REQUIRE(minioauth2::detail::base64_url_decode("FPucA9l-").value() == "\x14\xfb\x9c\x03\xd9\x7e"); 61 | REQUIRE(minioauth2::detail::base64_url_decode("FPucA9k").value() == "\x14\xfb\x9c\x03\xd9"); 62 | REQUIRE(minioauth2::detail::base64_url_decode("FPucAw").value() == "\x14\xfb\x9c\x03"); 63 | REQUIRE(minioauth2::detail::base64_url_decode("").value() == ""); 64 | REQUIRE(minioauth2::detail::base64_url_decode("Invalid!").has_value() == false); // Invalid char '!' 65 | } 66 | } 67 | 68 | TEST_CASE("Query Parameter Parsing", "[query]") { 69 | std::string q1 = "code=abc&state=xyz123"; 70 | auto p1 = minioauth2::parse_query_params(q1); 71 | REQUIRE(p1.size() == 2); 72 | REQUIRE(p1["code"] == "abc"); 73 | REQUIRE(p1["state"] == "xyz123"); 74 | 75 | std::string q2 = "error=access_denied&error_description=User+denied+access"; 76 | auto p2 = minioauth2::parse_query_params(q2); 77 | REQUIRE(p2.size() == 2); 78 | REQUIRE(p2["error"] == "access_denied"); 79 | REQUIRE(p2["error_description"] == "User denied access"); // Decoded '+' 80 | 81 | std::string q3 = "value=this%20has%20spaces%26stuff"; 82 | auto p3 = minioauth2::parse_query_params(q3); 83 | REQUIRE(p3.size() == 1); 84 | REQUIRE(p3["value"] == "this has spaces&stuff"); // Decoded hex 85 | 86 | std::string q4 = "flag1&flag2=&key=value"; 87 | auto p4 = minioauth2::parse_query_params(q4); 88 | REQUIRE(p4.size() == 3); 89 | REQUIRE(p4.count("flag1")); 90 | REQUIRE(p4["flag1"] == ""); // Key only 91 | REQUIRE(p4.count("flag2")); 92 | REQUIRE(p4["flag2"] == ""); // Key with empty value 93 | REQUIRE(p4["key"] == "value"); 94 | 95 | std::string q5 = ""; 96 | auto p5 = minioauth2::parse_query_params(q5); 97 | REQUIRE(p5.empty()); 98 | 99 | // Leading '=' or empty segments 100 | std::string q6 = "=val&key=&" ; 101 | auto p6 = minioauth2::parse_query_params(q6); 102 | REQUIRE(p6.size() == 2); // '=val' might be parsed as key="", value="val" or skipped, behavior can vary. Let's assume key="", value="val" 103 | REQUIRE(p6[""] == "val"); 104 | REQUIRE(p6["key"] == ""); 105 | } 106 | 107 | // TODO: Add tests for: 108 | // - generate_code_challenge (plain, S256 - needs mock SHA256 or real one) 109 | // - build_authorization_request (check URL components) 110 | // - build_token_exchange_request (check body components) 111 | // - parse_token_response (requires nlohmann/json) 112 | // - parse_jwt_payload (requires nlohmann/json) 113 | 114 | int main() { 115 | // Example test (very basic) 116 | std::string random_str = minioauth2::generate_random_string(32); 117 | assert(random_str.length() == 32); 118 | std::cout << "Basic test passed." << std::endl; 119 | return 0; 120 | } -------------------------------------------------------------------------------- /test/test_utils.cpp: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | #include "minioauth2.hpp" // To access functions to test 3 | 4 | // Test fixture for utility functions if needed, or just plain TEST_F/TEST 5 | 6 | TEST(Base64UrlEncodeTest, HandlesEmptyString) { 7 | EXPECT_EQ(minioauth2::detail::base64_url_encode(""), ""); 8 | } 9 | 10 | TEST(Base64UrlEncodeTest, EncodesSimpleString) { 11 | // "Hello" -> "SGVsbG8" 12 | EXPECT_EQ(minioauth2::detail::base64_url_encode("Hello"), "SGVsbG8"); 13 | } 14 | 15 | TEST(Base64UrlEncodeTest, EncodesStringRequiringPaddingRemoval) { 16 | // Standard Base64 for "Man" is "TWFu", no padding, so Base64URL is the same. 17 | EXPECT_EQ(minioauth2::detail::base64_url_encode("Man"), "TWFu"); 18 | // Standard Base64 for "Ma" is "TWE=", Base64URL is "TWE". 19 | EXPECT_EQ(minioauth2::detail::base64_url_encode("Ma"), "TWE"); 20 | // Standard Base64 for "M" is "TQ==", Base64URL is "TQ". 21 | EXPECT_EQ(minioauth2::detail::base64_url_encode("M"), "TQ"); 22 | } 23 | 24 | TEST(Base64UrlEncodeTest, HandlesURLSpecificCharacters) { 25 | // Test string with '+' and '/' which should be '-' and '_' in base64url 26 | // If our input for base64url_encode is raw bytes, it should just encode them. 27 | // If it were decoding, this would be more relevant. For encoding, any byte is valid input. 28 | // Let's test with bytes that would be problematic in standard base64 if not URL-safe. 29 | // Example: The string "?/>" contains characters that might be an issue in URLs or standard base64. 30 | // Bytes: 0x3f 0x2f 0x3e 31 | // Standard Base64: Pz4+ 32 | // URL-safe Base64: Pz4- 33 | // Our function takes raw bytes and produces base64url. So, if input has byte 0xfb then output char is '-' 34 | // Input: std::string("\xfb\xff\xfe", 3) -> base64: "-_-" (since 0xfb->+, 0xff->/, 0xfe->~ in some variants but we use standard table for values) 35 | // Let's use a known vector from RFC 4648 for base64url examples 36 | // For PKCE, verifiers are typically alphanumeric, so complex byte patterns are less common for sha256 input, 37 | // but the base64url encoder should be robust. 38 | // For `code_challenge = base64url(sha256(code_verifier))`, the input to base64url is raw binary data. 39 | 40 | std::string binary_input_1 = { (char)0x14, (char)0xfb, (char)0x9c, (char)0x03, (char)0xd9, (char)0x7e }; 41 | EXPECT_EQ(minioauth2::detail::base64_url_encode(binary_input_1), "FPucA9l-"); // Standard: FPucA9l+ 42 | 43 | std::string binary_input_2 = { (char)0x14, (char)0xfb, (char)0x9c, (char)0x03, (char)0xd9 }; 44 | EXPECT_EQ(minioauth2::detail::base64_url_encode(binary_input_2), "FPucA9k"); // Standard: FPucA9k= 45 | 46 | std::string binary_input_3 = { (char)0x14, (char)0xfb, (char)0x9c, (char)0x03 }; 47 | EXPECT_EQ(minioauth2::detail::base64_url_encode(binary_input_3), "FPucAw"); // Standard: FPucAw== 48 | } 49 | 50 | // TODO: Add tests for base64_url_decode 51 | // TODO: Add tests for generate_random_string (check length, charset, randomness is hard to test deterministically) 52 | // TODO: Add tests for generate_code_challenge (plain and S256, needs SHA256 mock or real impl) 53 | // TODO: Add tests for url_encode/url_decode 54 | // TODO: Add tests for parse_query_params 55 | // TODO: Add tests for build_authorization_request 56 | // TODO: Add tests for build_token_exchange_request 57 | // TODO: Add tests for parse_token_response (if nlohmann/json is enabled) -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minioauth2-project", 3 | "version-string": "0.1.0", 4 | "dependencies": [ 5 | "openssl" 6 | ] 7 | } --------------------------------------------------------------------------------