11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
# File 'lib/legion/api/auth.rb', line 11
def self.register_token_exchange(app)
app.post '/api/auth/token' do
Legion::Logging.debug "API: POST /api/auth/token params=#{params.keys}"
body = parse_request_body
grant_type = body[:grant_type]
subject_token = body[:subject_token]
unless grant_type == 'urn:ietf:params:oauth:grant-type:token-exchange'
Legion::Logging.warn "API POST /api/auth/token returned 400: unsupported grant_type=#{grant_type}"
halt 400, json_error('unsupported_grant_type', 'expected urn:ietf:params:oauth:grant-type:token-exchange',
status_code: 400)
end
unless subject_token
Legion::Logging.warn 'API POST /api/auth/token returned 400: subject_token is required'
halt 400, json_error('missing_subject_token', 'subject_token is required', status_code: 400)
end
unless defined?(Legion::Crypt::JWT) && Legion::Crypt::JWT.respond_to?(:verify_with_jwks)
halt 501, json_error('jwks_validation_not_available', 'legion-crypt JWKS support not loaded',
status_code: 501)
end
rbac_settings = (Legion::Settings[:rbac].is_a?(Hash) && Legion::Settings[:rbac][:entra]) || {}
tenant_id = rbac_settings[:tenant_id]
unless tenant_id
Legion::Logging.error 'API POST /api/auth/token returned 500: rbac.entra.tenant_id not set'
halt 500, json_error('entra_tenant_not_configured', 'rbac.entra.tenant_id not set', status_code: 500)
end
jwks_url = "https://login.microsoftonline.com/#{tenant_id}/discovery/v2.0/keys"
issuer = "https://login.microsoftonline.com/#{tenant_id}/v2.0"
begin
entra_claims = Legion::Crypt::JWT.verify_with_jwks(
subject_token, jwks_url: jwks_url, issuers: [issuer]
)
rescue Legion::Crypt::JWT::ExpiredTokenError
Legion::Logging.warn 'API POST /api/auth/token returned 401: Entra token has expired'
halt 401, json_error('token_expired', 'Entra token has expired', status_code: 401)
rescue Legion::Crypt::JWT::InvalidTokenError => e
Legion::Logging.warn "API POST /api/auth/token returned 401: #{e.message}"
halt 401, json_error('invalid_token', e.message, status_code: 401)
rescue Legion::Crypt::JWT::Error => e
Legion::Logging.error "API POST /api/auth/token returned 502: #{e.message}"
halt 502, json_error('identity_provider_unavailable', e.message, status_code: 502)
end
unless defined?(Legion::Rbac::EntraClaimsMapper)
halt 501, json_error('claims_mapper_not_available', 'legion-rbac EntraClaimsMapper not loaded',
status_code: 501)
end
mapped = Legion::Rbac::EntraClaimsMapper.map_claims(
entra_claims,
role_map: rbac_settings[:role_map] || Legion::Rbac::EntraClaimsMapper::DEFAULT_ROLE_MAP,
group_map: rbac_settings[:group_map] || {},
default_role: rbac_settings[:default_role] || 'worker'
)
ttl = 28_800
token = Legion::API::Token.issue_human_token(
msid: mapped[:sub], name: mapped[:name],
roles: mapped[:roles], ttl: ttl
)
Legion::Logging.info "API: issued human token for sub=#{mapped[:sub]} roles=#{mapped[:roles]&.join(',')}"
json_response({
access_token: token,
token_type: 'Bearer',
expires_in: ttl,
roles: mapped[:roles],
team: mapped[:team]
})
end
end
|