Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
37.11% covered (danger)
37.11%
36 / 97
45.45% covered (danger)
45.45%
5 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
SSO_Helpers
37.11% covered (danger)
37.11%
36 / 97
45.45% covered (danger)
45.45%
5 / 11
286.67
0.00% covered (danger)
0.00%
0 / 1
 generateToken
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 saveToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validateToken
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
7.48
 getUserIdFromToken
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getUserFromToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 logFailure
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 shouldThrottle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 triggerFailure
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 triggerSuccess
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getSuccessUrl
41.67% covered (danger)
41.67%
10 / 24
0.00% covered (danger)
0.00%
0 / 1
20.70
 handleLogin
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace NewfoldLabs\WP\Module\SSO;
4
5class SSO_Helpers {
6
7    /**
8     * SSO AJAX action.
9     */
10    const ACTION = 'newfold_sso_login';
11
12    /**
13     * SSO token meta key.
14     */
15    const META_KEY = 'newfold_sso_token';
16
17    /**
18     * Generate an SSO token for a user.
19     *
20     * @param int $user_id user id
21     *
22     * @return string
23     */
24    public static function generateToken( $user_id ) {
25        return base64_encode(
26            implode(
27                ':',
28                array(
29                    $user_id,
30                    time(),
31                    wp_generate_password( 64, true, true ),
32                )
33            )
34        );
35    }
36
37    /**
38     * Save an SSO token for a specific user.
39     *
40     * @param string $token
41     */
42    public static function saveToken( $token ) {
43        update_user_meta( self::getUserIdFromToken( $token ), self::META_KEY, $token );
44    }
45
46    /**
47     * Validate an SSO token.
48     *
49     * @param $token
50     *
51     * @return bool
52     */
53    public static function validateToken( $token ) {
54
55        // Decode token
56        $parts = explode( ':', base64_decode( $token ), 3 );
57
58        // Validate user
59        $user_id = absint( array_shift( $parts ) );
60        if ( ! $user_id ) {
61            return false;
62        }
63
64        $user = get_user_by( 'id', $user_id );
65        if ( ! $user || ! is_a( $user, \WP_User::class ) ) {
66            return false;
67        }
68
69        // Validate timeframe
70        $time = array_shift( $parts );
71        if ( ! $time || ( $time + 600 ) < time() ) {
72            return false;
73        }
74
75        // Validate token
76        $user_token = get_user_meta( $user->ID, self::META_KEY, true );
77        if ( $token !== $user_token ) {
78            return false;
79        }
80
81        // Token is valid
82        return true;
83    }
84
85    /**
86     * Get the WordPress user ID from a token.
87     *
88     * @param string $token
89     *
90     * @return int
91     */
92    public static function getUserIdFromToken( $token ) {
93        $parts = explode( ':', base64_decode( $token ), 3 );
94
95        return absint( array_shift( $parts ) );
96    }
97
98    /**
99     * Get the WordPress user object from a token.
100     *
101     * @param string $token
102     *
103     * @return \WP_User|false
104     */
105    public static function getUserFromToken( $token ) {
106        return get_user_by( 'id', self::getUserIdFromToken( $token ) );
107    }
108
109    /**
110     * Log a failed SSO attempt.
111     */
112    public static function logFailure() {
113        $key   = 'newfold_sso_failure_count';
114        $count = absint( get_transient( $key ) );
115        set_transient( $key, $count + 1, MINUTE_IN_SECONDS * 5 );
116    }
117
118    /**
119     * Check if we should throttle attempts.
120     *
121     * @return bool
122     */
123    public static function shouldThrottle() {
124        return absint( get_transient( 'newfold_sso_failure_count' ) ) > 4;
125    }
126
127    /**
128     * Trigger an SSO failure.
129     *
130     * @param string $error_type type of error
131     */
132    public static function triggerFailure( $error_type = '' ) {
133
134        self::logFailure();
135
136        // Enable legacy action when necessary
137        if ( has_action( 'eig_sso_fail' ) ) {
138            do_action( 'eig_sso_fail' );
139        }
140
141        do_action( 'newfold_sso_fail' );
142
143        if ( empty( $error_type ) ) {
144            wp_safe_redirect( wp_login_url() );
145        } else {
146            wp_safe_redirect(
147                add_query_arg(
148                    array(
149                        'error' => $error_type,
150                    ),
151                    wp_login_url()
152                )
153            );
154        }
155        exit;
156    }
157
158    /**
159     * Trigger an SSO success
160     *
161     * @param \WP_User $user
162     */
163    public static function triggerSuccess( \WP_User $user ) {
164
165        wp_set_current_user( $user->ID, $user->user_login );
166        wp_set_auth_cookie( $user->ID );
167        do_action( 'wp_login', $user->user_login, $user );
168
169        $redirect = self::getSuccessUrl();
170
171        // Enable legacy action when necessary
172        if ( has_action( 'eig_sso_success' ) ) {
173            do_action( 'eig_sso_success', $user, $redirect );
174        }
175
176        do_action( 'newfold_sso_success', $user, $redirect );
177
178        // Ensure the same token can't be used twice
179        delete_user_meta( $user->ID, self::META_KEY );
180
181        wp_safe_redirect( $redirect );
182        exit;
183    }
184
185    /**
186     * Get the SSO success URL.
187     *
188     * @return string
189     */
190    public static function getSuccessUrl() {
191        $url = '';
192
193        $params = array( 'bounce', 'redirect' );
194
195        foreach ( $params as $param ) {
196            $relative_path = esc_url_raw( filter_input( INPUT_GET, $param ) );
197            if ( $relative_path ) {
198                $url = admin_url( $relative_path );
199                break;
200            }
201        }
202
203        if ( $url ) {
204            $params = $_GET;
205
206            unset( $params['action'] );
207            unset( $params['bounce'] );
208            unset( $params['nonce'] );
209            unset( $params['redirect'] );
210            unset( $params['salt'] );
211            unset( $params['token'] );
212            unset( $params['user'] );
213
214            // Persist all query params not used for SSO
215            if ( ! empty( $params ) ) {
216                foreach ( $params as $key => $value ) {
217                    $url = add_query_arg( $key, $value, $url );
218                }
219            }
220        }
221
222        if ( ! $url ) {
223            $url = apply_filters( 'newfold_sso_success_url_default', admin_url() );
224        }
225
226        // Enable legacy filter when necessary
227        if ( has_filter( 'eig_sso_redirect' ) ) {
228            $url = (string) apply_filters( 'eig_sso_redirect', $url );
229        }
230
231        return (string) apply_filters( 'newfold_sso_success_url', $url );
232    }
233
234    /**
235     * Handle SSO login.
236     *
237     * @param string $token
238     */
239    public static function handleLogin( $token ) {
240
241        // No token provided
242        if ( ! $token ) {
243            wp_safe_redirect( wp_login_url() );
244            exit;
245        }
246
247        // Too many failed attempts
248        if ( self::shouldThrottle() ) {
249            self::triggerFailure();
250            exit;
251        }
252
253        $isValid = self::validateToken( $token );
254
255        // Invalid token
256        if ( ! $isValid ) {
257            self::triggerFailure();
258            exit;
259        }
260
261        $user = self::getUserFromToken( $token );
262        if ( $user ) {
263            if ( preg_match( "/['\"\\\\<|]/", $user->user_login ) ) {
264                self::triggerFailure( 'invalid_username' );
265                exit;
266            }
267        }
268        self::triggerSuccess( $user );
269    }
270}