Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Release

on:
pull_request:
types:
- closed
branches:
- main

jobs:
release:
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'version-bump')
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.SDK_BOT_APP_ID }}
private-key: ${{ secrets.SDK_BOT_PRIVATE_KEY }}

- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 0

- name: Get version from Version.php
id: version
run: |
VERSION=$(grep -oP "SDK_VERSION = '\K[0-9]+\.[0-9]+\.[0-9]+" lib/Version.php)
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Create Release
uses: softprops/action-gh-release@v2
with:
token: ${{ steps.app-token.outputs.token }}
tag_name: ${{ steps.version.outputs.version }}
name: ${{ steps.version.outputs.version }}
generate_release_notes: true
make_latest: true
80 changes: 80 additions & 0 deletions .github/workflows/version-bump.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Version Bump

on:
workflow_dispatch:
inputs:
bump_type:
description: 'Version bump type'
required: true
type: choice
options:
- patch
- minor
- major

jobs:
bump-version:
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.SDK_BOT_APP_ID }}
private-key: ${{ secrets.SDK_BOT_PRIVATE_KEY }}

- name: Checkout repository
uses: actions/checkout@v6
with:
token: ${{ steps.app-token.outputs.token }}

- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Bump version
id: bump
run: |
VERSION_FILE="lib/Version.php"
OLD_VERSION=$(grep -oP "SDK_VERSION = '\K[0-9]+\.[0-9]+\.[0-9]+" "$VERSION_FILE")

IFS='.' read -r MAJOR MINOR PATCH <<< "$OLD_VERSION"

case "${{ inputs.bump_type }}" in
major)
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
;;
minor)
MINOR=$((MINOR + 1))
PATCH=0
;;
patch)
PATCH=$((PATCH + 1))
;;
esac

NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"

sed -i "s/SDK_VERSION = '$OLD_VERSION'/SDK_VERSION = '$NEW_VERSION'/" "$VERSION_FILE"

echo "old_version=$OLD_VERSION" >> $GITHUB_OUTPUT
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT

- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ steps.app-token.outputs.token }}
branch: version-bump-${{ steps.bump.outputs.new_version }}
commit-message: "Bump version from ${{ steps.bump.outputs.old_version }} to ${{ steps.bump.outputs.new_version }}"
title: "Bump version to ${{ steps.bump.outputs.new_version }}"
body: |
This PR bumps the version from `${{ steps.bump.outputs.old_version }}` to `${{ steps.bump.outputs.new_version }}`.

**Bump type:** ${{ inputs.bump_type }}

---
*This PR was automatically created by the version bump workflow.*
labels: version-bump
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
],
"require": {
"php": ">=7.3.0",
"ext-curl": "*"
"ext-curl": "*",
"paragonie/halite": "^4.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.15|^3.6",
Expand Down
149 changes: 149 additions & 0 deletions lib/CookieSession.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

namespace WorkOS;

use WorkOS\Resource\SessionAuthenticationSuccessResponse;
use WorkOS\Resource\SessionAuthenticationFailureResponse;

/**
* Class CookieSession
*
* Handles encrypted session cookies for user authentication and session management.
* Matches workos-node CookieSession behavior - unsealing and validating sessions.
*/
class CookieSession
{
/**
* @var UserManagement
*/
private $userManagement;

/**
* @var string Encrypted session data
*/
private $sealedSession;

/**
* @var string Cookie encryption password
*/
private $cookiePassword;

/**
* Constructor.
*
* @param UserManagement $userManagement UserManagement instance
* @param string $sealedSession Encrypted session cookie data
* @param string $cookiePassword Password used to decrypt the session
*/
public function __construct(
UserManagement $userManagement,
string $sealedSession,
string $cookiePassword
) {
$this->userManagement = $userManagement;
$this->sealedSession = $sealedSession;
$this->cookiePassword = $cookiePassword;
}

/**
* Authenticates the sealed session and returns user information.
*
* @return SessionAuthenticationSuccessResponse|SessionAuthenticationFailureResponse
* @throws Exception\WorkOSException
*/
public function authenticate()
{
return $this->userManagement->authenticateWithSessionCookie(
$this->sealedSession,
$this->cookiePassword
);
}

/**
* Refreshes an expired session and returns new tokens.
*
* Note: This method returns raw tokens. The calling code (e.g., authkit-php)
* is responsible for sealing the tokens into a new session cookie.
*
* @param array $options Options for session refresh
* - 'organizationId' (string|null): Organization to scope the session to
*
* @return array{SessionAuthenticationSuccessResponse|SessionAuthenticationFailureResponse, array|null}
* Returns [response, newTokens] where newTokens contains:
* - 'access_token': The new access token
* - 'refresh_token': The new refresh token
* - 'session_id': The session ID
* Returns [failureResponse, null] on error.
* @throws Exception\WorkOSException
*/
public function refresh(array $options = [])
{
$organizationId = $options['organizationId'] ?? null;

// First authenticate to get the current session data
$authResult = $this->authenticate();

if (!$authResult->authenticated) {
return [$authResult, null];
}

// Tight try/catch for refresh token API call
try {
$refreshedAuth = $this->userManagement->authenticateWithRefreshToken(
WorkOS::getClientId(),
$authResult->refreshToken,
null,
null,
$organizationId
);
} catch (Exception\BaseRequestException $e) {
$failureResponse = new SessionAuthenticationFailureResponse(
SessionAuthenticationFailureResponse::REASON_HTTP_ERROR
);
return [$failureResponse, null];
}

// Build success response
$successResponse = SessionAuthenticationSuccessResponse::constructFromResponse([
'authenticated' => true,
'access_token' => $refreshedAuth->accessToken,
'refresh_token' => $refreshedAuth->refreshToken,
'session_id' => $authResult->sessionId,
'user' => $refreshedAuth->user->raw,
'organization_id' => $refreshedAuth->organizationId ?? $organizationId,
'authentication_method' => $authResult->authenticationMethod
]);

// Return raw tokens for the caller to seal
$newTokens = [
'access_token' => $refreshedAuth->accessToken,
'refresh_token' => $refreshedAuth->refreshToken,
'session_id' => $authResult->sessionId
];

return [$successResponse, $newTokens];
}

/**
* Gets the logout URL for the current session.
*
* @param array $options
* - 'returnTo' (string|null): URL to redirect to after logout
*
* @return string Logout URL
* @throws Exception\UnexpectedValueException
*/
public function getLogoutUrl(array $options = [])
{
$authResult = $this->authenticate();

if (!$authResult->authenticated) {
throw new Exception\UnexpectedValueException(
"Cannot get logout URL for unauthenticated session"
);
}

$returnTo = $options['returnTo'] ?? null;
return $this->userManagement->getLogoutUrl($authResult->sessionId, $returnTo);
}
}
54 changes: 54 additions & 0 deletions lib/Resource/Session.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace WorkOS\Resource;

/**
* Class Session.
*
* @property string $id
* @property string $userId
* @property string|null $ipAddress
* @property string|null $userAgent
* @property string|null $organizationId
* @property string $authenticationMethod
* @property string $status
* @property string $expiresAt
* @property string|null $endedAt
* @property string $createdAt
* @property string $updatedAt
* @property string $object
*/
class Session extends BaseWorkOSResource
{
public const RESOURCE_TYPE = "session";

public const RESOURCE_ATTRIBUTES = [
"id",
"userId",
"ipAddress",
"userAgent",
"organizationId",
"authenticationMethod",
"status",
"expiresAt",
"endedAt",
"createdAt",
"updatedAt",
"object"
];

public const RESPONSE_TO_RESOURCE_KEY = [
"id" => "id",
"user_id" => "userId",
"ip_address" => "ipAddress",
"user_agent" => "userAgent",
"organization_id" => "organizationId",
"authentication_method" => "authenticationMethod",
"status" => "status",
"expires_at" => "expiresAt",
"ended_at" => "endedAt",
"created_at" => "createdAt",
"updated_at" => "updatedAt",
"object" => "object"
];
}
43 changes: 43 additions & 0 deletions lib/Resource/SessionAuthenticationFailureResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace WorkOS\Resource;

/**
* Class SessionAuthenticationFailureResponse.
*
* Represents a failed session authentication.
*
* @property bool $authenticated
* @property string $reason
*/
class SessionAuthenticationFailureResponse extends BaseWorkOSResource
{
public const REASON_NO_SESSION_COOKIE_PROVIDED = "NO_SESSION_COOKIE_PROVIDED";
public const REASON_INVALID_SESSION_COOKIE = "INVALID_SESSION_COOKIE";
public const REASON_ENCRYPTION_ERROR = "ENCRYPTION_ERROR";
public const REASON_HTTP_ERROR = "HTTP_ERROR";

public const RESOURCE_ATTRIBUTES = [
"authenticated",
"reason"
];

public const RESPONSE_TO_RESOURCE_KEY = [
"authenticated" => "authenticated",
"reason" => "reason"
];

/**
* Construct a failure response with a specific reason.
*
* @param string $reason Reason for authentication failure
*/
public function __construct(string $reason)
{
$this->values = [
"authenticated" => false,
"reason" => $reason
];
$this->raw = [];
}
}
Loading