Jump to content
View in the app

A better way to browse. Learn more.

Invision Community

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

Featured Replies

Posted

Hello,

It's been a long time since I've done some php coding, and just getting back into it.

I am currently trying to create an OAuth connection to another ICS installation on a local install. For basics on testing, I am only trying to show user info after authenticated. Here is my setup:

Application.php

    /**
     * @brief API Configuration
     */
    protected static $apiConfig = [
        'key' => 'c41be4bf148ed229e2b86473c82c0ac3',
        'base_url' => 'https://localhost/invisionapi/api',
        'oauth' => [
            'scope' => 'profile email messages members',
            'authorize_url' => 'https://localhost/invisionapi/oauth/authorize',
            'token_url' => 'https://localhost/invisionapi/oauth/token'
        ],
    ];

    /**
     * @brief Get OAuth Configuration
     *
     * @return array
     */
    public static function getOAuthConfig(): array
    {
        return array_merge(static::$apiConfig['oauth'], [
            'client_id' => Settings::i()->api_client_id,
            'client_secret' => Settings::i()->api_client_secret,
            'redirect_uri' => Settings::i()->api_client_callback,
        ]);
    }

    /**
     * @brief Get API Base URL
     *
     * @return string
     */
    public static function getApiBaseUrl(): string
    {
        return static::$apiConfig['base_url'];
    }

    /**
     * @brief Make OAuth Request
     *
     * @param string $endpoint The API endpoint path
     * @param string $method HTTP method (GET, POST, etc)
     * @param array $parameters Request parameters
     * @param string|null $accessToken OAuth access token (optional, will use stored token if not provided)
     * @return array Response data
     * @throws \IPS\Http\Request\Exception
     */
    public static function oauth(string $endpoint, string $method = 'GET', array $parameters = [], ?string $accessToken = NULL): array
    {
        // Get access token if not provided
        if (!$accessToken) {
            $accessToken = static::getOAuthAccessToken();
            if (!$accessToken) {
                throw new \Exception('No access token available');
            }
        }
        
        // Build the full URL
        $url = Url::external(static::getApiBaseUrl() . '/' . ltrim($endpoint, '/'));
            
        if ($method === 'GET' && !empty($parameters)) {
            $url = $url->setQueryString($parameters);
        }
        
        try {
            $clientId = Settings::i()->api_client_id;
            
            $request = $url->request()
                ->setHeaders([
                    'Accept' => 'application/json',
                    'User-Agent' => 'Invision-Agent',
                    'Authorization' => 'Bearer ' . $accessToken,
                    'Client' => $clientId
                ]);
                
            // Make the request
            if ($method === 'GET') {
                $response = $request->get();
            } else {
                $response = $request->$method($parameters);
            }
            
            // Get the raw response content
            $content = (string) $response;
            
            $decoded = json_decode($content, TRUE);
            
            // Return empty array if decode failed or response is null
            return $decoded ?: [];
            
        } catch (\Exception $e) {
            // Try to refresh token if unauthorized
            if ($e->getCode() === 401) {
                static::refreshOAuthToken();
                return static::oauth($endpoint, $method, $parameters);
            }
            
            return []; // Return empty array instead of throwing
        }
    }

    /**
     * @brief Store OAuth tokens
     *
     * @param array $tokenData Array containing token information (access_token, refresh_token, expires_in)
     * @return void
     */
    public static function storeOAuthTokens(array $tokenData): void
    {
        // Calculate expiration timestamp
        $expiresAt = time() + ($tokenData['expires_in'] ?? 3600);
        
        // Store the tokens and expiration
        Db::i()->update('core_sys_conf_settings', array(
            'conf_value' => $tokenData['access_token']
        ), array('conf_key=?', 'oauth_access_token'));
        
        Db::i()->update('core_sys_conf_settings', array(
            'conf_value' => $tokenData['refresh_token']
        ), array('conf_key=?', 'oauth_refresh_token'));
        
        Db::i()->update('core_sys_conf_settings', array(
            'conf_value' => $expiresAt
        ), array('conf_key=?', 'oauth_expires_at'));
        
        // Update runtime cache
        Settings::i()->oauth_access_token = $tokenData['access_token'];
        Settings::i()->oauth_refresh_token = $tokenData['refresh_token'];
        Settings::i()->oauth_expires_at = $expiresAt;
        
        // Clear settings cache
        unset(Store::i()->settings);
    }
    
    /**
     * @brief Get stored OAuth access token
     *
     * @return string|null
     */
    public static function getOAuthAccessToken(): ?string
    {
        $settings = Settings::i();
        
        if (!$settings->oauth_access_token) {
            return NULL;
        }
        
        // Check if token is expired
        if ($settings->oauth_expires_at && $settings->oauth_expires_at < time()) {
            try {
                static::refreshOAuthToken();
            } catch (\Exception $e) {
                return NULL;
            }
        }
        
        return $settings->oauth_access_token;
    }
    
    /**
     * @brief Refresh OAuth token
     *
     * @return void
     * @throws \Exception
     */
    protected static function refreshOAuthToken(): void
    {
        $settings = Settings::i();
        
        if (!$settings->oauth_refresh_token) {
            throw new \Exception('No refresh token available');
        }
        
        $config = static::getOAuthConfig();
        
        $response = Url::external($config['token_url'])
            ->request()
            ->post([
                'grant_type' => 'refresh_token',
                'client_id' => $config['client_id'],
                'client_secret' => $config['client_secret'],
                'refresh_token' => $settings->oauth_refresh_token
            ]);
            
        $data = json_decode($response, TRUE);
        
        if (!isset($data['access_token'])) {
            throw new \Exception('Invalid token response');
        }
        
        // Update stored tokens
        $settings->oauth_access_token = $data['access_token'];
        $settings->oauth_refresh_token = $data['refresh_token'] ?? $settings->oauth_refresh_token;
        $settings->oauth_expires_at = time() + ($data['expires_in'] ?? 3600);
        $settings->save();
    }

    /**
     * @brief Start OAuth flow
     *
     * @param \IPS\Http\Url $returnUrl URL to return to after authentication
     * @return void
     * @throws \Exception
     */
    public static function startOAuthFlow(\IPS\Http\Url $returnUrl): void
    {
        $config = static::getOAuthConfig();
        
        // Store return URL in admin session data
        $_SESSION['oauth_return_url'] = (string)$returnUrl;
        
        // Generate and store state parameter
        $state = \IPS\Session::i()->csrfKey;
        $_SESSION['oauth_state'] = $state;
        
        // Build authorization URL
        $authUrl = Url::external($config['authorize_url'])
            ->setQueryString([
                'client_id' => $config['client_id'],
                'redirect_uri' => $config['redirect_uri'],
                'response_type' => 'code',
                'scope' => $config['scope'],
                'state' => $state
            ]);
            
        \IPS\Output::i()->redirect($authUrl);
    }
    
    /**
     * @brief Handle OAuth callback
     *
     * @param string $code Authorization code
     * @param string $state CSRF state parameter
     * @return void
     * @throws \Exception
     */
    public static function handleOAuthCallback(string $code, string $state): void
    {
        $config = static::getOAuthConfig();
        
        // Exchange code for access token
        $response = Url::external($config['token_url'])
            ->request()
            ->post([
                'grant_type' => 'authorization_code',
                'client_id' => $config['client_id'],
                'client_secret' => $config['client_secret'],
                'redirect_uri' => $config['redirect_uri'],
                'code' => $code
            ]);
            
        $data = json_decode($response, TRUE);
        
        if (!isset($data['access_token'])) {
            throw new \Exception('Invalid token response');
        }
        
        // Store tokens
        $settings = Settings::i();
        $settings->oauth_access_token = $data['access_token'];
        $settings->oauth_refresh_token = $data['refresh_token'] ?? NULL;
        $settings->oauth_expires_at = time() + ($data['expires_in'] ?? 3600);
        $settings->save();
    }

userInfo.php

    /**
     * @brief Manage method - shows the login form or redirects to OAuth
     *
     * @return void
     */
    protected function manage(): void
    {
        // Check if we already have a valid token
        $accessToken = Application::getOAuthAccessToken();
        if ($accessToken) {
            Output::i()->redirect(\IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo&do=showInfo'));
        }
        
        // If not logged in, show the OAuth login button
        Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('menu__myapp_userinfo_userinfo');
        Output::i()->output = \IPS\Theme::i()->getTemplate('userinfo', 'myapp', 'admin')->loginForm();
    }

    /**
     * @brief Start OAuth flow
     *
     * @return void
     */
    protected function oauth(): void
    {
        // Generate random state parameter
        $state = bin2hex(random_bytes(16));
        $_SESSION['oauth_state'] = $state;
        
        try {
            Application::startOAuthFlow(
                \IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo&do=showInfo'),
                $state
            );
        } catch (\Exception $e) {
            Output::i()->error('OAuth authentication failed: ' . $e->getMessage(), '500');
        }
    }
    
    /**
     * @brief OAuth callback handler
     *
     * @return void
     */
    protected function callback(): void
    {
        try {
            if (!Request::i()->code) {
                throw new \Exception('No authorization code received');
            }
            
            if (Request::i()->error) {
                $error = Request::i()->error_description ?: Request::i()->error;
                throw new \Exception('OAuth error: ' . $error);
            }
            
            
            
            // Verify state parameter matches CSRF key
            if (Request::i()->state !== \IPS\Session::i()->csrfKey) {
                throw new \Exception('Invalid state parameter');
            }
            
            Application::handleOAuthCallback(
                Request::i()->code,
                Request::i()->state
            );
            
            // Get the stored return URL or use default
            $returnUrl = isset($_SESSION['oauth_return_url']) 
                ? \IPS\Http\Url::createFromString($_SESSION['oauth_return_url'])
                : \IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo&do=showInfo', 'admin');
            
            // Add debug logging
            //if (\IPS\IN_DEV || \IPS\DEBUG_LOG) {
                \IPS\Log::debug(
                    "OAuth Callback Debug:\n" .
                    "Received State: " . (Request::i()->state ?: 'NULL') . "\n" .
                    "Session CSRF Key: " . \IPS\Session::i()->csrfKey . "\n" .
                    "Request Parameters: " . json_encode(Request::i(), JSON_PRETTY_PRINT) . "\n" .
                    "Session Data: " . json_encode($_SESSION, JSON_PRETTY_PRINT),
                    'oauth_callback'
                );
            //}
            
            // Clear the stored return URL
            unset($_SESSION['oauth_return_url']);
            
            // Redirect to return URL
            Output::i()->redirect($returnUrl);
            
        } catch (\Exception $e) {
            if (\IPS\IN_DEV || \IPS\DEBUG_LOG) {
                \IPS\Log::debug("OAuth Callback Error: " . $e->getMessage() . "\n" . $e->getTraceAsString(), 'oauth_callback');
            }
            
            // Redirect back to login form with error
            Output::i()->redirect(
                \IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo', 'admin')
                    ->setQueryString('error', $e->getMessage())
            );
        }
    }
    
    /**
     * @brief Show user information
     *
     * @return void
     */
    protected function showInfo(): void
    {
        $accessToken = Application::getOAuthAccessToken();
        
        if (!$accessToken) {
            Output::i()->redirect(\IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo'));
        }
        
        try {
            // Fetch user information using the OAuth token
            $userInfo = Application::oauth('user', 'GET', [], $accessToken);
            
            Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('menu__myapp_userinfo_userinfo');
            Output::i()->output = \IPS\Theme::i()->getTemplate('userinfo', 'myapp', 'admin')->userInfo($userInfo);
            
        } catch (\Exception $e) {
            Output::i()->error('Could not fetch user information: ' . $e->getMessage(), '500');
        }
    }
    
    /**
     * @brief Logout
     *
     * @return void
     */
    protected function logout(): void
    {
        // Clear stored OAuth tokens
        $settings = \IPS\Settings::i();
        $settings->oauth_access_token = NULL;
        $settings->oauth_refresh_token = NULL;
        $settings->oauth_expires_at = NULL;
        $settings->save();
        
        Output::i()->redirect(\IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo'));
    }

loginForm.phtml

<ips:template parameters="$error=NULL" />

<div class="ipsBox ipsShadow">
    <div class="ipsBox_container">
        <h2 class="ipsType_sectionTitle">Connect to Localhost API</h2>
        
        {{if !\IPS\Settings::i()->api_client_id || !\IPS\Settings::i()->api_client_secret || !\IPS\Settings::i()->api_client_callback}}
            <div class="ipsMessage ipsMessage_warning">
                <strong>OAuth Configuration Required</strong>
                <p>Please configure the OAuth client settings in the <a href="{url="app=myapp&module=dashboard&controller=dashboard&do=settings"}">Localhost API Settings</a> before connecting your account.</p>
            </div>
        {{else}}
            {{if $error}}
            <div class="ipsMessage ipsMessage_error">
                {$error}
            </div>
            {{endif}}
            
            <p class="ipsType_normal">Connect your Localhost API account to access additional features and manage your Localhost API presence.</p>
            
            <form action="{url="app=myapp&module=userinfo&controller=userinfo&do=oauth"}" method="post">
                <input type="hidden" name="csrfKey" value="{expression="\IPS\Session::i()->csrfKey"}" />
                
                <div class="ipsBox_controls ipsPad_half ipsClearfix">
                    <button type="submit" class="ipsButton ipsButton_primary ipsButton_large" {{if !\IPS\Settings::i()->api_client_id || !\IPS\Settings::i()->api_client_secret || !\IPS\Settings::i()->api_client_callback}}disabled{{endif}}>
                        <i class="fa fa-sign-in"></i> Connect with Localhost API
                    </button>
                </div>
            </form>
        {{endif}}
    </div>
</div>

I can get all the way to here:
image.png

After I click "Continue with NuclearGeneral", I am presented with the redirection url configured in settings and oauth client on localhost api, and this csrf error is displayed:

image.png

I have a debug log in the callback() method, but the log is never entered. I have tried several attempts, all at which don't let me authenticate and show the user info.

What am I missing here? Any help very much appreciated.

-Donald

  • Author

Bump.. can anyone help with this please? 🙏

  • Author

I tried updating the oauth() and callback() in the userinfo.php to this. I noticed I had the wrong endpoint for the user info. Changed 'user' to 'core/me' in the showInfo(). I still get a csrf error. I really don't see what could be wrong.. it should be saving the info recieved into the settings, and it should be giving me debug logs, but I am also not getting the debig logs recoreded in the ACP.

    /**
     * @brief Start OAuth flow
     *
     * @return void
     */
    protected function oauth(): void
    {
        // Start the session if not already started
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }

        // Generate and store state parameter
        $state = \IPS\Session::i()->csrfKey;
        $_SESSION['oauth_state'] = $state;
        
        try {
            Application::startOAuthFlow(
                \IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo&do=showInfo'),
                $state
            );
        } catch (\Exception $e) {
            \IPS\Log::debug("OAuth Start Error: " . $e->getMessage(), 'oauth_callback');
            Output::i()->error('OAuth authentication failed: ' . $e->getMessage(), '500');
        }
    }

    /**
     * @brief OAuth callback handler
     *
     * @return void
     */
    protected function callback(): void
    {
        // Start the session if not already started
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }

        try {
            if (!Request::i()->code) {
                throw new \Exception('No authorization code received');
            }
            
            if (Request::i()->error) {
                $error = Request::i()->error_description ?: Request::i()->error;
                throw new \Exception('OAuth error: ' . $error);
            }
            
            // Verify state parameter matches stored state
            if (!isset($_SESSION['oauth_state']) || Request::i()->state !== $_SESSION['oauth_state']) {
                \IPS\Log::debug(
                    "State mismatch:\nReceived: " . Request::i()->state . "\nStored: " . ($_SESSION['oauth_state'] ?? 'NOT SET'),
                    'oauth_callback'
                );
                throw new \Exception('Invalid state parameter');
            }
            
            Application::handleOAuthCallback(
                Request::i()->code,
                Request::i()->state
            );
            
            // Get the stored return URL or use default
            $returnUrl = isset($_SESSION['oauth_return_url']) 
                ? \IPS\Http\Url::createFromString($_SESSION['oauth_return_url'])
                : \IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo&do=showInfo', 'admin');
            
            // Add debug logging
            \IPS\Log::debug(
                "OAuth Callback Debug:\n" .
                "Received State: " . (Request::i()->state ?: 'NULL') . "\n" .
                "Session State: " . ($_SESSION['oauth_state'] ?? 'NULL') . "\n" .
                "Request Parameters: " . json_encode(Request::i(), JSON_PRETTY_PRINT) . "\n" .
                "Session Data: " . json_encode($_SESSION, JSON_PRETTY_PRINT),
                'oauth_callback'
            );
            
            // Clear the stored OAuth state and return URL
            unset($_SESSION['oauth_state']);
            unset($_SESSION['oauth_return_url']);
            
            // Redirect to return URL
            Output::i()->redirect($returnUrl);
            
        } catch (\Exception $e) {
            if (\IPS\IN_DEV || \IPS\DEBUG_LOG) {
                \IPS\Log::debug("OAuth Callback Error: " . $e->getMessage() . "\n" . $e->getTraceAsString(), 'oauth_callback');
            }
            
            // Redirect back to login form with error
            Output::i()->redirect(
                \IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo', 'admin')
                    ->setQueryString('error', $e->getMessage())
            );
        }
    }

    /**
     * @brief Show user information
     *
     * @return void
     */
    protected function showInfo(): void
    {
        $accessToken = Application::getOAuthAccessToken();
        
        if (!$accessToken) {
            Output::i()->redirect(\IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo'));
        }
        
        try {
            // Fetch user information using the OAuth token
            $userInfo = Application::oauth('core/me', 'GET', [], $accessToken);
            
            Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('menu__myapp_userinfo_userinfo');
            Output::i()->output = \IPS\Theme::i()->getTemplate('userinfo', 'myapp', 'admin')->userInfo($userInfo);
            
        } catch (\Exception $e) {
            Output::i()->error('Could not fetch user information: ' . $e->getMessage(), '500');
        }
    }

Why are you trying to recreate something that already exists?

When setting up an OAuth client in Invision Community, select 'Invision Community' as the type to connect to another Invision Community install.

  • Author

I'm not trying to connect two communities together. I am trying to display information from another community inside the ACP of the application I am creating, by using an Authenticated session from OAuth, displaying relative information from the user who logged in.

  • Author

Just for a little more context, I am trying to use the REST API with OAuth, to retrieve information from another ICS 5 community, and then save that OAuth login information in the database.

Edited by Nuclear General

I see.

By default, all AdminCP methods require CSRF protection. Your redirect back into the AdminCP does not contain a CSRFKey. There's a $csrfProtected property you can set on the class to remove this automatic protection, but you should only do so if you have adequate protection of your own.

There are also a couple of other issues I saw:

  • You cannot update the settings directly in the database and have them work (you also shouldn't do this). Look for \IPS\Settings::i()->changeValues(); instead.

  • You're also trying to remove those settings by writing them to the settings object and saving them. This also won't work; use the method above.

  • Author

If I'm understanding correctly, I've made some changes, and I still can't make a connection to save the details. I'm getting a little brain wrecked from trying to get this to work for almost a week.

I've also tried another variation of the settings for implicit login, and that still can't save.

Application.php

    /**
     * @brief API Configuration
     */
    protected static $apiConfig = [
        'key' => 'c41be4bf148ed229e2b86473c82c0ac3',
        'base_url' => 'https://localhost/invisionapi/api',
        'oauth' => [
            'scope' => 'profile email messages members',
            'authorize_url' => 'https://localhost/invisionapi/oauth/authorize',
            'token_url' => 'https://localhost/invisionapi/oauth/token'
        ],
    ];

    /**
     * @brief Get OAuth Configuration
     *
     * @return array
     */
    public static function getOAuthConfig(): array
    {
        return array_merge(static::$apiConfig['oauth'], [
            'client_id' => Settings::i()->api_client_id,
            'client_secret' => Settings::i()->api_client_secret,
            'redirect_uri' => Settings::i()->api_client_callback,
        ]);
    }

    /**
     * @brief Get API Base URL
     *
     * @return string
     */
    public static function getApiBaseUrl(): string
    {
        return static::$apiConfig['base_url'];
    }

    /**
     * @brief Make OAuth Request
     *
     * @param string $endpoint The API endpoint path
     * @param string $method HTTP method (GET, POST, etc)
     * @param array $parameters Request parameters
     * @param string|null $accessToken OAuth access token (optional, will use stored token if not provided)
     * @return array Response data
     * @throws \IPS\Http\Request\Exception
     */
    public static function oauth(string $endpoint, string $method = 'GET', array $parameters = [], ?string $accessToken = NULL): array
    {
        // Get access token if not provided
        if (!$accessToken) {
            $accessToken = static::getOAuthAccessToken();
            if (!$accessToken) {
                throw new \Exception('No access token available');
            }
        }
        
        // Build the full URL
        $url = Url::external(static::getApiBaseUrl() . '/' . ltrim($endpoint, '/'));
            
        if ($method === 'GET' && !empty($parameters)) {
            $url = $url->setQueryString($parameters);
        }
        
        try {
            $clientId = Settings::i()->api_client_id;
            
            $request = $url->request()
                ->setHeaders([
                    'Accept' => 'application/json',
                    'User-Agent' => 'Invision-Agent',
                    'Authorization' => 'Bearer ' . $accessToken,
                    'Client' => $clientId
                ]);
                
            // Make the request
            if ($method === 'GET') {
                $response = $request->get();
            } else {
                $response = $request->$method($parameters);
            }
            
            // Get the raw response content
            $content = (string) $response;
            
            $decoded = json_decode($content, TRUE);
            
            // Return empty array if decode failed or response is null
            return $decoded ?: [];
            
        } catch (\Exception $e) {
            // Try to refresh token if unauthorized
            if ($e->getCode() === 401) {
                static::refreshOAuthToken();
                return static::oauth($endpoint, $method, $parameters);
            }
            
            return []; // Return empty array instead of throwing
        }
    }

    /**
     * @brief Store OAuth tokens
     *
     * @param array $tokenData Array containing token information (access_token, refresh_token, expires_in)
     * @return void
     */
    public static function storeOAuthTokens(array $tokenData): void
    {
        // Calculate expiration timestamp
        $expiresAt = time() + ($tokenData['expires_in'] ?? 3600);
        
        // Update settings using the proper method
        \IPS\Settings::i()->changeValues([
            'oauth_access_token' => $tokenData['access_token'],
            'oauth_refresh_token' => $tokenData['refresh_token'] ?? NULL,
            'oauth_expires_at' => $expiresAt
        ]);
    }
    
    /**
     * @brief Get stored OAuth access token
     *
     * @return string|null
     */
    public static function getOAuthAccessToken(): ?string
    {
        $settings = Settings::i();
        
        if (!$settings->oauth_access_token) {
            return NULL;
        }
        
        // Check if token is expired
        if ($settings->oauth_expires_at && $settings->oauth_expires_at < time()) {
            try {
                static::refreshOAuthToken();
            } catch (\Exception $e) {
                return NULL;
            }
        }
        
        return $settings->oauth_access_token;
    }
    
    /**
     * @brief Refresh OAuth token
     *
     * @return void
     * @throws \Exception
     */
    protected static function refreshOAuthToken(): void
    {
        $settings = Settings::i();
        
        if (!$settings->oauth_refresh_token) {
            throw new \Exception('No refresh token available');
        }
        
        $config = static::getOAuthConfig();
        
        $response = Url::external($config['token_url'])
            ->request()
            ->post([
                'grant_type' => 'refresh_token',
                'client_id' => $config['client_id'],
                'client_secret' => $config['client_secret'],
                'refresh_token' => $settings->oauth_refresh_token
            ]);
            
        $data = json_decode($response, TRUE);
        
        if (!isset($data['access_token'])) {
            throw new \Exception('Invalid token response');
        }
        
        // Update stored tokens using proper method
            static::storeOAuthTokens([
                'access_token' => $data['access_token'],
                'refresh_token' => $data['refresh_token'] ?? NULL,
                'expires_in' => $data['expires_in'] ?? 3600
            ]);
    }

    /**
     * @brief Start OAuth flow
     *
     * @param \IPS\Http\Url $returnUrl URL to return to after authentication
     * @return void
     * @throws \Exception
     */
    public static function startOAuthFlow(\IPS\Http\Url $returnUrl): void
    {
        $config = static::getOAuthConfig();
        
        // Store return URL in admin session data
        $_SESSION['oauth_return_url'] = (string)$returnUrl;
        
        // Generate and store state parameter
        $state = \IPS\Session::i()->csrfKey;
        $_SESSION['oauth_state'] = $state;
        
        // Build authorization URL
        $authUrl = Url::external($config['authorize_url'])
            ->setQueryString([
                'client_id' => $config['client_id'],
                'redirect_uri' => $config['redirect_uri'],
                'response_type' => 'code',
                'scope' => $config['scope'],
                'state' => $state
            ]);
            
        \IPS\Output::i()->redirect($authUrl);
    }
    
    /**
     * @brief Handle OAuth callback
     *
     * @param string $code Authorization code
     * @param string $state CSRF state parameter
     * @return void
     * @throws \Exception
     */
    public static function handleOAuthCallback(string $code, string $state): void
    {
        $config = static::getOAuthConfig();
        
        // Exchange code for access token
        $response = Url::external($config['token_url'])
            ->request()
            ->post([
                'grant_type' => 'authorization_code',
                'client_id' => $config['client_id'],
                'client_secret' => $config['client_secret'],
                'redirect_uri' => $config['redirect_uri'],
                'code' => $code
            ]);
            
        $data = json_decode($response, TRUE);
        
        if (!isset($data['access_token'])) {
            throw new \Exception('Invalid token response');
        }
        
        // Store tokens using proper method
        static::storeOAuthTokens([
            'access_token' => $data['access_token'],
            'refresh_token' => $data['refresh_token'] ?? NULL,
            'expires_in' => $data['expires_in'] ?? 3600
        ]);
    }

userInfo.php

    /**
     * @brief Manage method - shows the login form or redirects to OAuth
     *
     * @return void
     */
    protected function manage(): void
    {
        // Check if we already have a valid token
        $accessToken = Application::getOAuthAccessToken();
        if ($accessToken) {
            Output::i()->redirect(\IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo&do=showInfo'));
        }
        
        // If not logged in, show the OAuth login button
        Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('menu__myapp_userinfo_userinfo');
        Output::i()->output = \IPS\Theme::i()->getTemplate('userinfo', 'myapp', 'admin')->loginForm();
    }

    /**
     * @brief Start OAuth flow
     *
     * @return void
     */
    protected function oauth(): void
    {
        $state = \IPS\Session::i()->csrfKey;
        $_SESSION['oauth_state'] = $state;
        
        try {
            Application::startOAuthFlow(
                \IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo&do=showInfo'),
                $state
            )->csrf();
        } catch (\Exception $e) {
            Output::i()->error('OAuth authentication failed: ' . $e->getMessage(), '500');
        }
    }
    
    /**
     * @brief OAuth callback handler
     *
     * @return void
     */
    protected function callback(): void
    {
        try {
            if (!Request::i()->code) {
                throw new \Exception('No authorization code received');
            }
            
            if (Request::i()->error) {
                $error = Request::i()->error_description ?: Request::i()->error;
                throw new \Exception('OAuth error: ' . $error);
            }
            
            
            
            // Verify state parameter matches CSRF key
            if (Request::i()->state !== \IPS\Session::i()->csrfKey) {
                throw new \Exception('Invalid state parameter');
            }
            
            Application::handleOAuthCallback(
                Request::i()->code,
                Request::i()->state
            );
            
            // Get the stored return URL or use default
            $returnUrl = isset($_SESSION['oauth_return_url']) 
                ? \IPS\Http\Url::createFromString($_SESSION['oauth_return_url'])
                : \IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo&do=showInfo', 'admin');
            
            // Add debug logging
            //if (\IPS\IN_DEV || \IPS\DEBUG_LOG) {
                \IPS\Log::debug(
                    "OAuth Callback Debug:\n" .
                    "Received State: " . (Request::i()->state ?: 'NULL') . "\n" .
                    "Session CSRF Key: " . \IPS\Session::i()->csrfKey . "\n" .
                    "Request Parameters: " . json_encode(Request::i(), JSON_PRETTY_PRINT) . "\n" .
                    "Session Data: " . json_encode($_SESSION, JSON_PRETTY_PRINT),
                    'oauth_callback'
                );
            //}
            
            // Clear the stored return URL
            unset($_SESSION['oauth_return_url']);
            
            // Redirect to return URL
            Output::i()->redirect($returnUrl);
            
        } catch (\Exception $e) {
            if (\IPS\IN_DEV || \IPS\DEBUG_LOG) {
                \IPS\Log::debug("OAuth Callback Error: " . $e->getMessage() . "\n" . $e->getTraceAsString(), 'oauth_callback');
            }
            
            // Redirect back to login form with error
            Output::i()->redirect(
                \IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo', 'admin')
                    ->setQueryString('error', $e->getMessage())
            );
        }
    }
    
    /**
     * @brief Show user information
     *
     * @return void
     */
    protected function showInfo(): void
    {
        $accessToken = Application::getOAuthAccessToken();
        
        if (!$accessToken) {
            Output::i()->redirect(\IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo'));
        }
        
        try {
            // Fetch user information using the OAuth token
            $userInfo = Application::oauth('user', 'GET', [], $accessToken);
            
            Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('menu__myapp_userinfo_userinfo');
            Output::i()->output = \IPS\Theme::i()->getTemplate('userinfo', 'myapp', 'admin')->userInfo($userInfo);
            
        } catch (\Exception $e) {
            Output::i()->error('Could not fetch user information: ' . $e->getMessage(), '500');
        }
    }
    
    /**
     * @brief Logout
     *
     * @return void
     */
    protected function logout(): void
    {
        // Clear stored OAuth tokens using proper method
        \IPS\Settings::i()->changeValues([
            'marketplace_oauth_access_token' => NULL,
            'marketplace_oauth_refresh_token' => NULL,
            'marketplace_oauth_expires_at' => NULL
        ]);
        
        Output::i()->redirect(\IPS\Http\Url::internal('app=myapp&module=userinfo&controller=userinfo'));
    }

Edited by Nuclear General

  • Author

OMG!! 😲😲 I think I've finally gotten it! 😁 And I'm tired and exhausted now lol 😴 I am now able to see the access tokens appear in the ACP of the installation that provides the API, and save that information in the database settings. But for now, I'm going to bed 🛌 haha 😂

Recently Browsing 0

  • No registered users viewing this page.

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.