Created
October 30, 2025 20:56
-
-
Save blashbrook/707e16012f577d3598e66d14eb8079eb to your computer and use it in GitHub Desktop.
Install EntraSSO Package into existing Laravel Application
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Laravel Entra SSO Setup Script | |
| # This script sets up an existing Laravel app to use the Entra SSO package | |
| set -e | |
| # Colors for output | |
| GREEN='\033[0;32m' | |
| BLUE='\033[0;34m' | |
| YELLOW='\033[1;33m' | |
| RED='\033[0;31m' | |
| NC='\033[0m' # No Color | |
| echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββ${NC}" | |
| echo -e "${BLUE}β Laravel Entra SSO Setup β${NC}" | |
| echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββ${NC}" | |
| echo "" | |
| # Check if we're in a Laravel directory | |
| if [ ! -f "artisan" ]; then | |
| echo -e "${RED}Error: Not in a Laravel directory!${NC}" | |
| echo "Please run this script from your Laravel project root." | |
| exit 1 | |
| fi | |
| echo -e "${GREEN}β Laravel project detected${NC}" | |
| echo "" | |
| # Prompt for package location | |
| read -p "Enter path to entra-sso package (default: ../entra-sso): " package_path | |
| package_path=${package_path:-"../entra-sso"} | |
| if [ ! -d "$package_path" ]; then | |
| echo -e "${RED}Error: Package directory not found at ${package_path}${NC}" | |
| exit 1 | |
| fi | |
| echo -e "${GREEN}β Package found${NC}" | |
| echo "" | |
| # Get vendor name from package composer.json | |
| vendor_name=$(grep -o '"name": "[^"]*"' ${package_path}/composer.json | head -1 | cut -d'"' -f4) | |
| echo -e "${BLUE}Step 1: Adding package to composer.json${NC}" | |
| # Ensure minimum-stability is set to dev | |
| if grep -q "\"minimum-stability\"" composer.json; then | |
| if [ "$(uname)" = "Darwin" ]; then | |
| sed -i '' 's/"minimum-stability": "[^"]*"/"minimum-stability": "dev"/' composer.json | |
| else | |
| sed -i 's/"minimum-stability": "[^"]*"/"minimum-stability": "dev"/' composer.json | |
| fi | |
| echo -e "${GREEN}β Minimum stability updated to dev${NC}" | |
| else | |
| if [ "$(uname)" = "Darwin" ]; then | |
| sed -i '' '/"name":/a\ | |
| "minimum-stability": "dev", | |
| ' composer.json | |
| else | |
| sed -i '/"name":/a\ "minimum-stability": "dev",' composer.json | |
| fi | |
| echo -e "${GREEN}β Minimum stability set to dev${NC}" | |
| fi | |
| # Check if repository already exists | |
| if grep -q "\"type\": \"path\"" composer.json 2>/dev/null; then | |
| echo -e "${YELLOW}Repository already exists in composer.json${NC}" | |
| else | |
| # Add repository to composer.json | |
| if [ "$(uname)" == "Darwin" ]; then | |
| # macOS | |
| sed -i '' '/"require": {/i\ | |
| "repositories": [\ | |
| {\ | |
| "type": "path",\ | |
| "url": "'${package_path}'"\ | |
| }\ | |
| ], | |
| ' composer.json | |
| else | |
| # Linux | |
| sed -i '/"require": {/i\ "repositories": [\n {\n "type": "path",\n "url": "'${package_path}'"\n }\n ],' composer.json | |
| fi | |
| echo -e "${GREEN}β Repository added${NC}" | |
| fi | |
| # Add package to require section | |
| if grep -q "${vendor_name}" composer.json; then | |
| echo -e "${YELLOW}Package already in composer.json${NC}" | |
| else | |
| # Add to require | |
| if [ "$(uname)" == "Darwin" ]; then | |
| sed -i '' 's|\"require\": {|\"require\": {\ | |
| \"'${vendor_name}'\": \"*\",|' composer.json | |
| else | |
| sed -i 's|\"require\": {|\"require\": {\\n \"'${vendor_name}'\": \"*\",|' composer.json | |
| fi | |
| echo -e "${GREEN}β Package added to require${NC}" | |
| fi | |
| echo "" | |
| echo -e "${BLUE}Step 2: Installing package${NC}" | |
| composer update ${vendor_name} | |
| echo -e "${GREEN}β Package installed${NC}" | |
| echo "" | |
| echo -e "${BLUE}Step 3: Publishing configuration${NC}" | |
| php artisan vendor:publish --tag=entra-sso-config | |
| echo -e "${GREEN}β Configuration published${NC}" | |
| echo "" | |
| echo -e "${BLUE}Step 4: Copying migration file${NC}" | |
| cp ${package_path}/database/migrations/*_add_entra_fields_to_users.php database/migrations/ | |
| echo -e "${GREEN}β Migration file copied${NC}" | |
| echo "" | |
| echo -e "${BLUE}Step 5: Updating User model${NC}" | |
| # Backup User model | |
| cp app/Models/User.php app/Models/User.php.backup | |
| # Check if already using EntraUser | |
| if grep -q "use Dcplibrary\\\\EntraSSO\\\\Models\\\\User" app/Models/User.php; then | |
| echo -e "${YELLOW}User model already extends EntraUser${NC}" | |
| else | |
| # Replace the Authenticatable import | |
| if [ "$(uname)" == "Darwin" ]; then | |
| sed -i '' "s/use Illuminate\\\\Foundation\\\\Auth\\\\User as Authenticatable;/use Dcplibrary\\\\EntraSSO\\\\Models\\\\User as EntraUser;/" app/Models/User.php | |
| else | |
| sed -i "s/use Illuminate\\\\Foundation\\\\Auth\\\\User as Authenticatable;/use Dcplibrary\\\\EntraSSO\\\\Models\\\\User as EntraUser;/" app/Models/User.php | |
| fi | |
| # Replace the class extension | |
| if [ "$(uname)" == "Darwin" ]; then | |
| sed -i '' "s/class User extends Authenticatable/class User extends EntraUser/" app/Models/User.php | |
| else | |
| sed -i "s/class User extends Authenticatable/class User extends EntraUser/" app/Models/User.php | |
| fi | |
| echo -e "${GREEN}β User model updated to extend EntraUser${NC}" | |
| echo -e "${YELLOW} Note: The EntraUser base class provides:${NC}" | |
| echo -e "${YELLOW} - entra_id, role, entra_groups, entra_custom_claims (fillable)${NC}" | |
| echo -e "${YELLOW} - Array casts for entra_groups and entra_custom_claims${NC}" | |
| echo -e "${YELLOW} - Helper methods: hasRole(), inGroup(), isAdmin(), etc.${NC}" | |
| echo -e "${YELLOW} (Backup saved as app/Models/User.php.backup)${NC}" | |
| fi | |
| echo "" | |
| echo -e "${BLUE}Step 6: Creating views${NC}" | |
| # Create auth directory if it doesn't exist | |
| mkdir -p resources/views/auth | |
| # Create login view | |
| if [ -f "resources/views/auth/login.blade.php" ]; then | |
| echo -e "${YELLOW}Login view already exists, creating login-sso.blade.php${NC}" | |
| view_file="resources/views/auth/login-sso.blade.php" | |
| else | |
| view_file="resources/views/auth/login.blade.php" | |
| fi | |
| cat > ${view_file} << 'LOGIN_EOF' | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Login - {{ config('app.name') }}</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 20px; | |
| } | |
| .login-container { | |
| background: white; | |
| border-radius: 16px; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| padding: 48px; | |
| max-width: 400px; | |
| width: 100%; | |
| } | |
| .logo { | |
| text-align: center; | |
| margin-bottom: 32px; | |
| } | |
| .logo h1 { | |
| font-size: 32px; | |
| font-weight: 700; | |
| color: #1a202c; | |
| margin-bottom: 8px; | |
| } | |
| .logo p { | |
| color: #718096; | |
| font-size: 14px; | |
| } | |
| .sso-button { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 100%; | |
| padding: 14px 24px; | |
| background: #0078d4; | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| transition: all 0.3s ease; | |
| border: none; | |
| cursor: pointer; | |
| } | |
| .sso-button:hover { | |
| background: #005a9e; | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 16px rgba(0, 120, 212, 0.3); | |
| } | |
| .sso-button svg { | |
| margin-right: 12px; | |
| } | |
| .error { | |
| background: #fee; | |
| border: 1px solid #fcc; | |
| color: #c33; | |
| padding: 12px; | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| font-size: 14px; | |
| } | |
| .divider { | |
| display: flex; | |
| align-items: center; | |
| text-align: center; | |
| margin: 24px 0; | |
| } | |
| .divider::before, | |
| .divider::after { | |
| content: ''; | |
| flex: 1; | |
| border-bottom: 1px solid #e2e8f0; | |
| } | |
| .divider span { | |
| padding: 0 16px; | |
| color: #718096; | |
| font-size: 14px; | |
| } | |
| .footer { | |
| text-align: center; | |
| margin-top: 24px; | |
| color: #718096; | |
| font-size: 12px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="login-container"> | |
| <div class="logo"> | |
| <h1>{{ config('app.name') }}</h1> | |
| <p>Sign in to continue</p> | |
| </div> | |
| @if($errors->any()) | |
| <div class="error"> | |
| {{ $errors->first() }} | |
| </div> | |
| @endif | |
| <a href="{{ route('entra.login') }}" class="sso-button"> | |
| <svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| <rect width="10" height="10" fill="currentColor"/> | |
| <rect x="11" width="10" height="10" fill="currentColor" opacity="0.8"/> | |
| <rect y="11" width="10" height="10" fill="currentColor" opacity="0.8"/> | |
| <rect x="11" y="11" width="10" height="10" fill="currentColor" opacity="0.6"/> | |
| </svg> | |
| Sign in with Microsoft | |
| </a> | |
| <div class="footer"> | |
| Secured by Azure Active Directory | |
| </div> | |
| </div> | |
| </body> | |
| </html> | |
| LOGIN_EOF | |
| echo -e "${GREEN}β Login view created: ${view_file}${NC}" | |
| # Create dashboard view | |
| cat > resources/views/dashboard.blade.php << 'DASHBOARD_EOF' | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Dashboard - {{ config('app.name') }}</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| background: #f7fafc; | |
| color: #2d3748; | |
| } | |
| .navbar { | |
| background: white; | |
| border-bottom: 1px solid #e2e8f0; | |
| padding: 16px 24px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .navbar h1 { | |
| font-size: 24px; | |
| font-weight: 700; | |
| } | |
| .logout-btn { | |
| background: #e53e3e; | |
| color: white; | |
| border: none; | |
| padding: 10px 20px; | |
| border-radius: 6px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .logout-btn:hover { | |
| background: #c53030; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 40px auto; | |
| padding: 0 24px; | |
| } | |
| .welcome-card { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 32px; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | |
| margin-bottom: 24px; | |
| } | |
| .welcome-card h2 { | |
| font-size: 28px; | |
| margin-bottom: 16px; | |
| color: #1a202c; | |
| } | |
| .info-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 24px; | |
| margin-top: 24px; | |
| } | |
| .info-card { | |
| background: #f7fafc; | |
| border: 1px solid #e2e8f0; | |
| border-radius: 8px; | |
| padding: 20px; | |
| } | |
| .info-card h3 { | |
| font-size: 14px; | |
| color: #718096; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| margin-bottom: 8px; | |
| } | |
| .info-card p { | |
| font-size: 18px; | |
| font-weight: 600; | |
| color: #2d3748; | |
| } | |
| .badge { | |
| display: inline-block; | |
| padding: 4px 12px; | |
| border-radius: 12px; | |
| font-size: 14px; | |
| font-weight: 600; | |
| margin: 4px; | |
| } | |
| .badge-admin { | |
| background: #fef5e7; | |
| color: #d68910; | |
| } | |
| .badge-user { | |
| background: #e8f5e9; | |
| color: #388e3c; | |
| } | |
| .badge-group { | |
| background: #e3f2fd; | |
| color: #1976d2; | |
| } | |
| ul { | |
| list-style: none; | |
| margin-top: 12px; | |
| } | |
| ul li { | |
| padding: 8px 0; | |
| border-bottom: 1px solid #e2e8f0; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <nav class="navbar"> | |
| <h1>{{ config('app.name') }}</h1> | |
| <form method="POST" action="{{ route('entra.logout') }}"> | |
| @csrf | |
| <button type="submit" class="logout-btn">Logout</button> | |
| </form> | |
| </nav> | |
| <div class="container"> | |
| <div class="welcome-card"> | |
| <h2>Welcome, {{ auth()->user()->name }}! π</h2> | |
| <p style="color: #718096; margin-top: 8px;">You're successfully logged in via Microsoft Entra SSO</p> | |
| </div> | |
| <div class="info-grid"> | |
| <div class="info-card"> | |
| <h3>Email</h3> | |
| <p>{{ auth()->user()->email }}</p> | |
| </div> | |
| <div class="info-card"> | |
| <h3>Role</h3> | |
| <p> | |
| <span class="badge {{ auth()->user()->role === 'admin' ? 'badge-admin' : 'badge-user' }}"> | |
| {{ ucfirst(auth()->user()->role) }} | |
| </span> | |
| </p> | |
| </div> | |
| @if(auth()->user()->entra_id) | |
| <div class="info-card"> | |
| <h3>Entra ID</h3> | |
| <p style="font-size: 14px; word-break: break-all;">{{ auth()->user()->entra_id }}</p> | |
| </div> | |
| @endif | |
| </div> | |
| @if(auth()->user()->entra_groups && count(auth()->user()->entra_groups) > 0) | |
| <div class="welcome-card" style="margin-top: 24px;"> | |
| <h3 style="margin-bottom: 16px;">Your Azure AD Groups</h3> | |
| <div> | |
| @foreach(auth()->user()->entra_groups as $group) | |
| <span class="badge badge-group">{{ $group }}</span> | |
| @endforeach | |
| </div> | |
| </div> | |
| @endif | |
| @if(auth()->user()->entra_custom_claims && count(auth()->user()->entra_custom_claims) > 0) | |
| <div class="welcome-card" style="margin-top: 24px;"> | |
| <h3 style="margin-bottom: 16px;">Custom Claims</h3> | |
| <ul> | |
| @foreach(auth()->user()->entra_custom_claims as $key => $value) | |
| <li> | |
| <strong>{{ $key }}:</strong> {{ is_array($value) ? json_encode($value) : $value }} | |
| </li> | |
| @endforeach | |
| </ul> | |
| </div> | |
| @endif | |
| </div> | |
| </body> | |
| </html> | |
| DASHBOARD_EOF | |
| echo -e "${GREEN}β Dashboard view created${NC}" | |
| echo "" | |
| echo -e "${BLUE}Step 7: Updating routes${NC}" | |
| # Add routes if not already present | |
| if grep -q "route('entra.login')" routes/web.php 2>/dev/null; then | |
| echo -e "${YELLOW}Routes already configured${NC}" | |
| else | |
| cat >> routes/web.php << 'ROUTES_EOF' | |
| // SSO Routes (provided by package automatically) | |
| // Login page | |
| Route::get('/login', function () { | |
| return view('auth.login'); | |
| })->name('login'); | |
| // Dashboard (protected) | |
| Route::middleware('auth')->group(function () { | |
| Route::get('/dashboard', function () { | |
| return view('dashboard'); | |
| })->name('dashboard'); | |
| }); | |
| ROUTES_EOF | |
| echo -e "${GREEN}β Routes added${NC}" | |
| fi | |
| echo "" | |
| echo -e "${BLUE}Step 8: Environment configuration${NC}" | |
| # Prompt for Azure credentials | |
| echo "" | |
| echo -e "${YELLOW}Please enter your Azure AD credentials:${NC}" | |
| read -p "Tenant ID: " tenant_id | |
| read -p "Client ID: " client_id | |
| read -p "Client Secret: " client_secret | |
| # Add to .env if not already present | |
| if grep -q "ENTRA_TENANT_ID" .env; then | |
| echo -e "${YELLOW}Entra configuration already in .env${NC}" | |
| echo -e "${YELLOW}Please update manually if needed${NC}" | |
| else | |
| cat >> .env << ENV_EOF | |
| # Entra SSO Configuration | |
| ENTRA_TENANT_ID=${tenant_id} | |
| ENTRA_CLIENT_ID=${client_id} | |
| ENTRA_CLIENT_SECRET=${client_secret} | |
| ENTRA_REDIRECT_URI="\${APP_URL}/auth/entra/callback" | |
| ENTRA_AUTO_CREATE_USERS=true | |
| ENTRA_SYNC_GROUPS=true | |
| ENTRA_SYNC_ON_LOGIN=true | |
| ENTRA_DEFAULT_ROLE=user | |
| ENTRA_ENABLE_TOKEN_REFRESH=true | |
| ENTRA_REFRESH_THRESHOLD=5 | |
| ENTRA_STORE_CUSTOM_CLAIMS=false | |
| ENV_EOF | |
| echo -e "${GREEN}β Environment variables added${NC}" | |
| fi | |
| echo "" | |
| echo -e "${BLUE}Step 9: Running migrations${NC}" | |
| read -p "Run migrations now? (y/n): " run_migrations | |
| if [ "$run_migrations" = "y" ] || [ "$run_migrations" = "Y" ]; then | |
| php artisan migrate | |
| echo -e "${GREEN}β Migrations completed${NC}" | |
| else | |
| echo -e "${YELLOW}β Remember to run: php artisan migrate${NC}" | |
| fi | |
| echo "" | |
| echo -e "${GREEN}ββββββββββββββββββββββββββββββββββββββββ${NC}" | |
| echo -e "${GREEN}β Setup Complete!${NC}" | |
| echo -e "${GREEN}ββββββββββββββββββββββββββββββββββββββββ${NC}" | |
| echo "" | |
| echo -e "${BLUE}Next Steps:${NC}" | |
| echo " 1. Configure Azure AD application (if not done)" | |
| echo " 2. Update role mappings in config/entra-sso.php" | |
| echo " 3. Start Laravel: ${YELLOW}php artisan serve${NC}" | |
| echo " 4. Visit: ${YELLOW}http://localhost:8000/login${NC}" | |
| echo " 5. Click 'Sign in with Microsoft'" | |
| echo "" | |
| echo -e "${BLUE}Documentation:${NC}" | |
| echo " - Installation: ${package_path}/docs/INSTALLATION.md" | |
| echo " - Azure Setup: ${package_path}/docs/AZURE_SETUP.md" | |
| echo " - Custom Claims: ${package_path}/docs/CUSTOM_CLAIMS.md" | |
| echo "" | |
| echo -e "${GREEN}Happy coding! π${NC}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment