Skip to content
Open
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
62 changes: 62 additions & 0 deletions app/bin/migrations/3.0.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
BEGIN;
SET FOREIGN_KEY_CHECKS = 0;

-- 1. Create accounts table (safe to run if already exists)
CREATE TABLE IF NOT EXISTS `accounts` (
`login` varchar(128) NOT NULL,
`type` char(16) NOT NULL DEFAULT 'login',
`user_id` int(11) NOT NULL,
`settings` json DEFAULT NULL,
PRIMARY KEY (`login`, `type`),
KEY `idx_accounts_user_id` (`user_id`),
CONSTRAINT `fk_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 2. Copy primary accounts from users.login
INSERT IGNORE INTO `accounts` (`login`, `user_id`, `type`)
SELECT LOWER(`login`), `user_id`, 'login' FROM `users`;

-- 3. Copy secondary accounts from users.login2 where they exist and are not duplicates
INSERT IGNORE INTO `accounts` (`login`, `user_id`, `type`)
SELECT LOWER(`login2`), `user_id`, 'login' FROM `users`
WHERE `login2` IS NOT NULL AND `login2` != '';

-- 4. Copy email from users.settings.email where set and not already in accounts
INSERT IGNORE INTO `accounts` (`login`, `user_id`, `type`, `settings`)
SELECT LOWER(JSON_UNQUOTE(JSON_EXTRACT(`settings`, '$.email'))), `user_id`, 'email',
JSON_OBJECT(
'email', JSON_OBJECT(
'verified', COALESCE(JSON_EXTRACT(`settings`, '$.email_verified'), 0)
)
)
FROM `users`
WHERE JSON_EXTRACT(`settings`, '$.email') IS NOT NULL
AND JSON_EXTRACT(`settings`, '$.email') != 'null';

-- 4.1 Remove migrated legacy email fields from users.settings
UPDATE `users`
SET `settings` = JSON_REMOVE(`settings`, '$.email', '$.email_verified')
WHERE JSON_CONTAINS_PATH(`settings`, 'one', '$.email', '$.email_verified');

-- 4.2 Copy telegram user_id from users.settings.telegram.user_id as login
INSERT IGNORE INTO `accounts` (`login`, `user_id`, `type`, `settings`)
SELECT JSON_UNQUOTE(JSON_EXTRACT(`settings`, '$.telegram.user_id')),
`user_id`, 'telegram',
JSON_OBJECT('telegram', JSON_EXTRACT(`settings`, '$.telegram'))
FROM `users`
WHERE JSON_EXTRACT(`settings`, '$.telegram.user_id') IS NOT NULL
AND JSON_EXTRACT(`settings`, '$.telegram.user_id') != 'null'
AND JSON_UNQUOTE(JSON_EXTRACT(`settings`, '$.telegram.user_id')) != '';

-- 4.3 Remove migrated telegram settings from users.settings
-- UPDATE `users`
-- SET `settings` = JSON_REMOVE(`settings`, '$.telegram')
-- WHERE JSON_CONTAINS_PATH(`settings`, 'one', '$.telegram');

-- 5. Remove login2 column from users (run after verifying the migration above)
-- ALTER TABLE `users` DROP KEY `users_uniq`;
-- ALTER TABLE `users` DROP KEY `users_uniq_login2`;
-- ALTER TABLE `users` DROP COLUMN `login2`;

SET FOREIGN_KEY_CHECKS = 1;
COMMIT;
2 changes: 1 addition & 1 deletion app/bin/shm-server.pl
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ sub log_msg {

# Save clean environment before any request processing
my %CLEAN_ENV = %ENV;
my $max_requests = 1000;
my $max_requests = $ENV{DEV} ? 1 : 1000;
my $request_count = 0;

while ($request_count < $max_requests && $request->Accept() >= 0) {
Expand Down
138 changes: 138 additions & 0 deletions app/lib/Core/Auth/Base.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package Core::Auth::Base;

use v5.14;
use parent 'Core::Base';
use Core::Base;
use Core::Const;
use Core::System::ServiceManager qw( get_service );
use URI::Escape qw( uri_escape );

sub provider_names {
my $self = shift;
my @providers = @_ ? @_ : ( OAUTH2_PROVIDERS );
return sort grep { $self->can("${_}_config") } @providers;
}

sub provider_config {
my $self = shift;
my $provider = shift || return undef;
my $method = "${provider}_config";
return $self->can($method) ? $self->$method() : undef;
}

sub normalize_claims {
my $self = shift;
my $provider = shift;
my $raw = shift || {};
my $method = "${provider}_normalize_claims";
return $self->can($method) ? $self->$method($raw) : {};
}

sub provider_fields {
my $self = shift;
my $provider = shift;
my $cfg = $self->can('oauth2_config') ? $self->oauth2_config($provider) : {};
$cfg = $self->can('oidc_config') ? $self->oidc_config($provider) : $cfg if ref($cfg) ne 'HASH';

my $fields = $cfg->{fields};
return ref $fields eq 'ARRAY' && @$fields ? $fields : ['email'];
}

sub provider_scope {
my $self = shift;
my $provider = shift;

my $pcfg = $self->provider_config($provider) || return '';
my %requested = map { $_ => 1 } @{ $self->provider_fields($provider) };
$requested{email} = 1; # email always required

my @scopes = ( $pcfg->{base_scope} || () );
for my $field ( sort keys %requested ) {
push @scopes, $pcfg->{field_scopes}{$field} if $pcfg->{field_scopes}{$field};
}
return join(' ', grep { length } @scopes);
}

sub http_transport {
my $self = shift;
return $self->{http_transport} ||= get_service('Transport::Http');
}

sub callback_url {
my $self = shift;
my $provider = shift;
my $prefix = shift || 'oauth2';

my $scheme = $ENV{HTTP_X_FORWARDED_PROTO}
|| ( ($ENV{HTTPS} || '') =~ /^(1|on)$/i ? 'https' : 'http' );
my $host = $ENV{HTTP_X_FORWARDED_HOST} || $ENV{HTTP_HOST} || 'localhost';

return sprintf('%s://%s/shm/v1/%s/callback/%s', $scheme, $host, $prefix, $provider);
}

sub auth_state_cache_key {
my $self = shift;
my $state = shift;
my $prefix = shift || 'auth_state';
return sprintf('%s_%s', $prefix, $state || '');
}

sub resolve_callback_state {
my $self = shift;
my $args = shift || {};
my $prefix = shift || 'auth_state';

my $state = $args->{state};
return $args unless $state;

my $ctx = cache->get_json( $self->auth_state_cache_key( $state, $prefix ) );
return $args unless ref $ctx eq 'HASH';

$args->{expected_state} //= $ctx->{state};
$args->{code_verifier} //= $ctx->{code_verifier};
$args->{nonce} //= $ctx->{nonce};
$args->{provider} //= $ctx->{provider};
$args->{redirect_uri} //= $ctx->{redirect_uri};
$args->{return_url} //= $ctx->{return_url} if defined $ctx->{return_url};
$args->{register_if_not_exists} = $ctx->{register_if_not_exists}
if !$args->{register_if_not_exists} && defined $ctx->{register_if_not_exists};
$args->{bind_to_profile} = $ctx->{bind_to_profile}
if !$args->{bind_to_profile} && defined $ctx->{bind_to_profile};
$args->{bind_only_if_new} = $ctx->{bind_only_if_new}
if !$args->{bind_only_if_new} && defined $ctx->{bind_only_if_new};
$args->{session_id} //= $ctx->{session_id} if defined $ctx->{session_id};
$args->{device_id} //= $ctx->{device_id} if defined $ctx->{device_id};

cache->delete( $self->auth_state_cache_key( $state, $prefix ) );
return $args;
}

sub build_callback_redirect_url {
my $self = shift;
my $result = shift;
my $return_url = shift;
my $default_error = shift || 'Authentication failed';
my $status_key = shift || 'oidc_status';

return undef unless $return_url;

my %query;
if ( ref $result eq 'HASH' && $result->{session_id} ) {
%query = ( $status_key => 'success', session_id => $result->{session_id} );
} elsif ( ref $result eq 'HASH' && ( $result->{error} || '' ) =~ /already linked/i ) {
%query = ( $status_key => 'already_exists', error => $result->{error} );
} elsif ( ref $result eq 'HASH' && ( $result->{error} || '' ) =~ /Already\s+bound/i ) {
%query = ( $status_key => 'already_bound', error => $result->{error} );
} elsif ( ref $result eq 'HASH' && ( $result->{msg} || '' ) =~ /Successfully\s+bound/i ) {
%query = ( $status_key => 'success', msg => $result->{msg} );
} elsif ( ref $result eq 'HASH' && $result->{error} ) {
%query = ( $status_key => 'error', error => $result->{error} );
} else {
%query = ( $status_key => 'error', error => $default_error );
}

my $sep = ( $return_url =~ /\?/ ) ? '&' : '?';
return $return_url . $sep . join('&', map { uri_escape($_) . '=' . uri_escape($query{$_}) } sort keys %query);
}

1;
Loading