Lines 81.25% 65 / 80
Methods 70.58% 12 / 17
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 2 / 2 100.00% 1 / 1 1
 create 16.66% 2 / 12 0.00% 0 / 1 4.31
 update 100.00% 1 / 1 100.00% 1 / 1 1
 delete 100.00% 1 / 1 100.00% 1 / 1 1
 checkCredentials 50.00% 1 / 2 0.00% 0 / 1 1.12
 isValidLogin 100.00% 3 / 3 100.00% 1 / 1 2
 authorize 100.00% 20 / 20 100.00% 1 / 1 1
 logout 100.00% 1 / 1 100.00% 1 / 1 1
 createOAuthChallenge 100.00% 13 / 13 100.00% 1 / 1 4
 [phpMyFAQ\Auth] getEncryptionContainer 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Auth] getErrors 100.00% 2 / 2 100.00% 1 / 1 3
 [phpMyFAQ\Auth] addError 0.00% 0 / 1 0.00% 0 / 1 2
 [phpMyFAQ\Auth] selectAuth 80.00% 8 / 10 0.00% 0 / 1 4.13
 [phpMyFAQ\Auth] enableReadOnly 100.00% 3 / 3 100.00% 1 / 1 1
 [phpMyFAQ\Auth] disableReadOnly 100.00% 3 / 3 100.00% 1 / 1 1
 [phpMyFAQ\Auth] isReadOnly 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Auth] encrypt 66.66% 2 / 3 0.00% 0 / 1 2.15
37class AuthEntraId extends Auth implements AuthDriverInterface
38{
39    private string $oAuthVerifier = '';
40
41    private string $oAuthChallenge;
42
43    private const string ENTRAID_CHALLENGE_METHOD = 'S256';
44
45    private const string ENTRAID_LOGOUT_URL = 'https://login.microsoftonline.com/common/wsfederation?wa=wsignout1.0';
46
47    /**
48     * @inheritDoc
49     */
50    public function __construct(
51        Configuration $configuration,
52        private readonly OAuth $oAuth,
53    ) {
54        $this->configuration = $configuration;
55
56        parent::__construct($configuration);
57    }
58
59    /**
60     * @inheritDoc
61     * @throws Exception
62     */
63    public function create(string $login, #[SensitiveParameter] string $password, string $domain = ''): mixed
64    {
65        $result = false;
66        $user = new User($this->configuration);
67
68        try {
69            $result = $user->createUser($login, '', $domain);
70        } catch (\Exception $exception) {
71            $this->configuration->getLogger()->info($exception->getMessage());
72        }
73
74        $user->setStatus('active');
75        $user->setAuthSource(AuthenticationSourceType::AUTH_AZURE->value);
76
77        // Set user information from JWT
78        $user->setUserData([
79            'display_name' => $this->oAuth->getName(),
80            'email' => $this->oAuth->getMail(),
81        ]);
82
83        return $result;
84    }
85
86    /**
87     * @inheritDoc
88     */
89    public function update(string $login, #[SensitiveParameter] string $password): bool
90    {
91        return true;
92    }
93
94    /**
95     * @inheritDoc
96     */
97    public function delete(string $login): bool
98    {
99        return true;
100    }
101
102    /**
103     * @inheritDoc
104     * @throws Exception
105     */
106    public function checkCredentials(
107        string $login,
108        #[SensitiveParameter]
109        string $password,
110        ?array $optionalData = [],
111    ): bool {
112        $this->create($login, '');
113        return true;
114    }
115
116    /**
117     * @inheritDoc
118     */
119    public function isValidLogin(string $login, ?array $optionalData = []): int
120    {
121        if ($login === $this->oAuth->getMail()) {
122            return 1;
123        }
124
125        return 0;
126    }
127
128    /**
129     * Method to authorize against Entra ID
130     *
131     * @throws \Exception
132     */
133    public function authorize(): RedirectResponse
134    {
135        $this->createOAuthChallenge();
136        $this->oAuth->getEntraIdSession()->setCurrentSessionKey();
137        $this->oAuth->getEntraIdSession()->set(EntraIdSession::ENTRA_ID_OAUTH_VERIFIER, $this->oAuthVerifier);
138        $this->oAuth->getEntraIdSession()->setCookie(
139            EntraIdSession::ENTRA_ID_OAUTH_VERIFIER,
140            $this->oAuthVerifier,
141            7200,
142            false,
143        );
144
145        $oAuthURL = sprintf(
146            'https://login.microsoftonline.com/%s/oauth2/v2.0/authorize'
147            . '?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&code_challenge=%s&code_challenge_method=%s',
148            AAD_OAUTH_TENANTID,
149            AAD_OAUTH_CLIENTID,
150            urlencode($this->configuration->getDefaultUrl() . 'services/azure/callback.php'),
151            AAD_OAUTH_SCOPE,
152            $this->oAuthChallenge,
153            self::ENTRAID_CHALLENGE_METHOD,
154        );
155
156        return new RedirectResponse($oAuthURL);
157    }
158
159    /**
160     * Logout
161     *
162     */
163    public function logout(): RedirectResponse
164    {
165        return new RedirectResponse(self::ENTRAID_LOGOUT_URL);
166    }
167
168    /**
169     * Method to generate code verifier and code challenge for oAuth login.
170     * See RFC7636 for details.
171     *
172     * @throws \Exception
173     */
174    private function createOAuthChallenge(): void
175    {
176        $verifier = $this->oAuthVerifier;
177
178        if ($this->oAuthVerifier === '' || $this->oAuthVerifier === '0') {
179            $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~';
180            $charLen = strlen($chars) - 1;
181            $verifier = '';
182
183            for ($i = 0; $i < 128; ++$i) {
184                $verifier .= $chars[random_int(0, $charLen)];
185            }
186
187            $this->oAuthVerifier = $verifier;
188        }
189
190        $this->oAuthChallenge = str_replace(
191            search: '=',
192            replace: '',
193            subject: strtr(string: base64_encode(pack('H*', hash('sha256', $verifier))), from: '+/', to: '-_'),
194        );
195    }
196}

Inherited from phpMyFAQ\Auth

73    public function getEncryptionContainer(string $encType): Encryption
74    {
75        $this->encContainer = Encryption::getInstance($encType, $this->configuration);
76        return $this->encContainer;
77    }
82    public function getErrors(): string
83    {
84        $message = $this->errors !== [] ? implode(separator: PHP_EOL, array: $this->errors) . PHP_EOL : '';
85        return $message . ($this->encContainer instanceof \phpMyFAQ\Encryption ? $this->encContainer->error() : '');
86    }
91    public function addError(string $message): void
92    {
93        $this->errors[] = $message;
94    }
101    public function selectAuth(string $method): Auth&AuthDriverInterface
102    {
103        $method = ucfirst(strtolower($method));
104        $authClass = '\\phpMyFAQ\\Auth\\Auth' . $method;
105
106        if (!class_exists($authClass) || !is_subclass_of($authClass, self::class)) {
107            $this->errors[] = self::PMF_ERROR_USER_NO_AUTH_TYPE;
108            throw new Exception(message: self::PMF_ERROR_USER_NO_AUTH_TYPE);
109        }
110
111        $auth = new $authClass($this->configuration);
112        if (!$auth instanceof AuthDriverInterface) {
113            $this->errors[] = self::PMF_ERROR_USER_NO_AUTH_TYPE;
114            throw new Exception(message: self::PMF_ERROR_USER_NO_AUTH_TYPE);
115        }
116
117        return $auth;
118    }
123    public function enableReadOnly(): bool
124    {
125        $oldReadOnly = $this->readOnly;
126        $this->readOnly = true;
127
128        return $oldReadOnly;
129    }
134    public function disableReadOnly(): bool
135    {
136        $oldReadOnly = $this->readOnly;
137        $this->readOnly = false;
138
139        return $oldReadOnly;
140    }
145    public function isReadOnly(): bool
146    {
147        return $this->readOnly;
148    }
155    public function encrypt(#[SensitiveParameter] string $string): string
156    {
157        if (!$this->encContainer instanceof \phpMyFAQ\Encryption) {
158            throw new Exception(message: 'No encryption container configured. Call getEncryptionContainer() first.');
159        }
160
161        return $this->encContainer->encrypt($string);
162    }